From bc2a6c70996bb346ffca98f7b8f495dc4ad7f2f7 Mon Sep 17 00:00:00 2001 From: Jeremy Poulin Date: Thu, 29 Nov 2018 16:08:12 -0500 Subject: [PATCH] Release v1.2.1 (#33) * First pass at adding support for ks_meta and other beaker overrides * NoOpProvisioner support. (#24) * First pass at adding a NoOpProvisioner * Cleaned up linter issues with linchpin passthrough and NoOpProvisioning. Also completed initial implementation of NoOpProvisioner. * Added basic test for NoOpProvisioning and enhanced info propagation. * Terrible hack to create new TargetHosts * Fixed issue with inventory file not being saved in inventoryPath var * Tried to add explicit root user since last test failed. But manual test works * Added cinch groups to layout file * Updating inventory path so that Cinch group_vars are picked up * Added tests for broken inventory paths * Brought code coverage back to 100% and fixed a bug with constructor params not propagating for hosts * Added pwd to lib test * Fixed PinFile, updated hostRequires, and allowed passthrough of distro and variant (#25) * Updated param type for hostrequires * Adding copy over for distro and variant * Ensuring the correct PinFile is used for provisioning * Fixed syntax error on endifs * Reverted provisioning conf and fixed provision test * LinchPin passthrough arguments and SSH mode script output archiving. (#26) * Updated param type for hostrequires * Adding copy over for distro and variant * Ensuring the correct PinFile is used for provisioning * Fixed syntax error on endifs * Reverted provisioning conf and fixed provision test * Started adding support for output synchronization in SSH mode * Added scriptParams object and fixed try/catch * Trying to move playbooks into resources * Adding the key file specification back to the playbook command * Added missing quotes * Writing out the file so that we can pass it as a path * Fixed syntax error with playbooks * Debugging failing playbooks * Moved playbook after the extra vars spec * Adding quotes around the invalid JSON * Adding missing quotes x2 * Adding rsync install role to run_scripts * Create a tmp directory to house results * Adding the directory keyword so that it gets created * Removing playbooks directory from paths * Moved lookup paths to correspond with new structure * Adding -x to ensure the script gets echo'd * Teardown rework and SSH output synchronization (#27) * Ensuring the correct PinFile is used for provisioning * Reverted provisioning conf and fixed provision test * Started adding support for output synchronization in SSH mode * Trying to move playbooks into resources * Adding the key file specification back to the playbook command * Added missing quotes * Debugging failing playbooks * Adding quotes around the invalid JSON * Adding missing quotes x2 * Adding rsync install role to run_scripts * Create a tmp directory to house results * Adding the directory keyword so that it gets created * Removing playbooks directory from paths * Moved lookup paths to correspond with new structure * Adding -x to ensure the script gets echo'd * Added logs to track error propagation * Reworked sections to ensure errors will trigger teardown when possible * Added support for host type override * Altered provisioning logic to prevent double calls into linchpin provisioner * Removed debug logs * Cleaned up error messaging for provisioning failures * Removed teardown log * Upgrading release version (#28) * Release version and linchpin distro/variant defaults fix (#29) * Upgrading release version * Ansible defaults choose null (a.k.a. 'None') over the default value * Disabling codenarc in favor of having working assignments (#30) * Added architecture to output file name to ensure parallel arch results are saved (#31) * manual install rsync (#32) --- resources/collect_results.yml | 17 ++ resources/run_scripts.yml | 42 ++++ src/com/redhat/ci/Job.groovy | 6 + src/com/redhat/ci/Utils.groovy | 38 ++-- src/com/redhat/ci/host/Type.groovy | 3 +- src/com/redhat/ci/hosts/Host.groovy | 6 + .../redhat/ci/hosts/ProvisionedHost.groovy | 8 + src/com/redhat/ci/hosts/TargetHost.groovy | 21 +- src/com/redhat/ci/provider/Type.groovy | 1 + .../redhat/ci/provisioner/Provisioner.groovy | 1 + .../ci/provisioner/ProvisioningConfig.groovy | 2 +- .../ci/provisioner/ProvisioningService.groovy | 116 ++++++----- src/com/redhat/ci/provisioner/Type.groovy | 3 +- .../provisioners/AbstractProvisioner.groovy | 45 +++++ .../provisioners/LinchPinProvisioner.groovy | 136 ++++++++----- .../ci/provisioners/NoOpProvisioner.groovy | 188 ++++++++++++++++++ test/PipelineTestScript.groovy | 5 + test/TestUtilsTest.groovy | 18 +- test/com/redhat/ci/JobTest.groovy | 18 +- test/com/redhat/ci/UtilsTest.groovy | 8 + .../ProvisioningServiceTest.groovy | 61 +++++- .../LinchPinProvisionerTest.groovy | 52 +++-- .../provisioners/NoOpProvisionerTest.groovy | 151 ++++++++++++++ vars/TestUtils.groovy | 12 ++ vars/runTests.groovy | 42 +++- workspace/PinFile | 13 +- 26 files changed, 847 insertions(+), 166 deletions(-) create mode 100644 resources/collect_results.yml create mode 100644 resources/run_scripts.yml create mode 100644 src/com/redhat/ci/provisioners/NoOpProvisioner.groovy create mode 100644 test/com/redhat/ci/provisioners/NoOpProvisionerTest.groovy diff --git a/resources/collect_results.yml b/resources/collect_results.yml new file mode 100644 index 0000000..bd4ce84 --- /dev/null +++ b/resources/collect_results.yml @@ -0,0 +1,17 @@ +--- +- name: Collect Artifacts and Reports + hosts: all + gather_facts: true + + vars_prompt: + - name: test_dir + prompt: "Please enter the path to your tests" + + tasks: + - debug: msg="{{ test_dir }}" + + - synchronize: + src: "/tmp/tests/" + dest: "{{ test_dir }}" + mode: pull + ignore_errors: true diff --git a/resources/run_scripts.yml b/resources/run_scripts.yml new file mode 100644 index 0000000..7a2cee4 --- /dev/null +++ b/resources/run_scripts.yml @@ -0,0 +1,42 @@ +--- +- name: "Run Scripts on each Inventory Host" + hosts: all + gather_facts: true + vars_prompt: + - name: test_dir + prompt: "Please enter the path to your tests" + + tasks: + - debug: + msg: "{{ test_dir }}" + + - package: + name: "{{ item }}" + state: latest + with_items: + - rsync + - libselinux-python + - python2 + + - file: + path: "/tmp/{{ test_dir }}" + state: directory + + - synchronize: + src: "{{ test_dir }}/scripts" + dest: "/tmp/{{ test_dir }}" + mode: push + ignore_errors: true + + - find: + recurse: false + paths: "/tmp/{{ test_dir }}/scripts" + file_type: directory + register: scripts + + - shell: "mkdir -p artifacts; bash -x test.sh {{ script_args | default('') }} &> artifacts/{{ ansible_architecture }}-output.txt" + args: + chdir: "{{ script_dir }}" + loop: "{{ scripts.files | map(attribute='path') | list }}" + loop_control: + loop_var: script_dir diff --git a/src/com/redhat/ci/Job.groovy b/src/com/redhat/ci/Job.groovy index 016ab35..f6cb097 100644 --- a/src/com/redhat/ci/Job.groovy +++ b/src/com/redhat/ci/Job.groovy @@ -93,6 +93,12 @@ class Job { } protected void teardown(ProvisionedHost host) { + if (!host || !host.provisioner) { + // If there isn't a host or a set provisioner, skip teardown + script.echo('Skipping teardown since host is null or host.provisioner is not set') + return + } + try { // Ensure teardown runs before the pipeline exits provSvc.teardown(host, config, script) diff --git a/src/com/redhat/ci/Utils.groovy b/src/com/redhat/ci/Utils.groovy index b88dcea..c271589 100644 --- a/src/com/redhat/ci/Utils.groovy +++ b/src/com/redhat/ci/Utils.groovy @@ -9,7 +9,7 @@ import com.redhat.ci.provisioner.Mode * Utility class to perform actions upon CI hosts. */ class Utils { - private static final String SUDO = 'sudo' + private static final String SUDO = 'sudo ' private static final String NO_SUDO = '' private static final String INSTALL_FILE = 'install.sh' @@ -22,12 +22,12 @@ class Utils { privileged, sh -> String sudo = privileged ? SUDO : NO_SUDO sh(""" - ${sudo} yum install python-devel openssl-devel libffi-devel -y && - ${sudo} mkdir -p /home/jenkins && - ${sudo} chown --recursive \${USER}:\${USER} /home/jenkins && - ${sudo} pip install --upgrade pip && - ${sudo} pip install --upgrade setuptools && - ${sudo} pip install --upgrade ansible + ${sudo}yum install python2-devel openssl-devel libffi-devel -y && + ${sudo}mkdir -p /home/jenkins && + ${sudo}chown --recursive \${USER}:\${USER} /home/jenkins && + ${sudo}pip install --upgrade pip && + ${sudo}pip install --upgrade setuptools && + ${sudo}pip install --upgrade ansible """) if (host == null) { return @@ -55,12 +55,12 @@ class Utils { ]) { script.env.HOME = '/home/jenkins' sh(""" - ${sudo} yum install -y krb5-workstation - ${sudo} cp ${script.KRBCONF} /etc/krb5.conf - ${sudo} mkdir -p /etc/beaker - ${sudo} cp ${script.BKRCONF} /etc/beaker/client.conf - ${sudo} chmod 644 /etc/krb5.conf - ${sudo} chmod 644 /etc/beaker/client.conf + ${sudo}yum install -y krb5-workstation + ${sudo}cp ${script.KRBCONF} /etc/krb5.conf + ${sudo}mkdir -p /etc/beaker + ${sudo}cp ${script.BKRCONF} /etc/beaker/client.conf + ${sudo}chmod 644 /etc/krb5.conf + ${sudo}chmod 644 /etc/beaker/client.conf kinit ${script.KRB_PRINCIPAL} -k -t ${script.KEYTAB} mkdir -p ~/.ssh cp ${script.SSHPRIVKEY} ~/.ssh/id_rsa @@ -86,15 +86,15 @@ class Utils { privileged, sh -> String sudo = privileged ? SUDO : NO_SUDO sh(""" - echo "pkgs.devel.redhat.com,10.19.208.80 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAplqWKs26qsoaTxvWn3DFcdbiBxqRLhFngGiMYhbudnAj4li9/VwAJqLm1M6YfjOoJrj9dlmuXhNzkSzvyoQODaRgsjCG5FaRjuN8CSM/y+glgCYsWX1HFZSnAasLDuW0ifNLPR2RBkmWx61QKq+TxFDjASBbBywtupJcCsA5ktkjLILS+1eWndPJeSUJiOtzhoN8KIigkYveHSetnxauxv1abqwQTk5PmxRgRt20kZEFSRqZOJUlcl85sZYzNC/G7mneptJtHlcNrPgImuOdus5CW+7W49Z/1xqqWI/iRjwipgEMGusPMlSzdxDX4JzIx6R53pDpAwSAQVGDz4F9eQ==" | ${sudo} tee -a /etc/ssh/ssh_known_hosts + echo "pkgs.devel.redhat.com,10.19.208.80 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAplqWKs26qsoaTxvWn3DFcdbiBxqRLhFngGiMYhbudnAj4li9/VwAJqLm1M6YfjOoJrj9dlmuXhNzkSzvyoQODaRgsjCG5FaRjuN8CSM/y+glgCYsWX1HFZSnAasLDuW0ifNLPR2RBkmWx61QKq+TxFDjASBbBywtupJcCsA5ktkjLILS+1eWndPJeSUJiOtzhoN8KIigkYveHSetnxauxv1abqwQTk5PmxRgRt20kZEFSRqZOJUlcl85sZYzNC/G7mneptJtHlcNrPgImuOdus5CW+7W49Z/1xqqWI/iRjwipgEMGusPMlSzdxDX4JzIx6R53pDpAwSAQVGDz4F9eQ==" | ${sudo}tee -a /etc/ssh/ssh_known_hosts - echo "Host pkgs.devel.redhat.com" | ${sudo} tee -a /etc/ssh/ssh_config - echo "IdentityFile /home/jenkins/.ssh/id_rsa" | ${sudo} tee -a /etc/ssh/ssh_config + echo "Host pkgs.devel.redhat.com" | ${sudo}tee -a /etc/ssh/ssh_config + echo "IdentityFile /home/jenkins/.ssh/id_rsa" | ${sudo}tee -a /etc/ssh/ssh_config - ${sudo} yum install -y yum-utils git + ${sudo}yum install -y yum-utils git curl -L -O http://download.devel.redhat.com/rel-eng/internal/rcm-tools-rhel-7-server.repo - ${sudo} yum-config-manager --add-repo rcm-tools-rhel-7-server.repo - ${sudo} yum install -y rhpkg + ${sudo}yum-config-manager --add-repo rcm-tools-rhel-7-server.repo + ${sudo}yum install -y rhpkg git config --global user.name "jenkins" """) if (host != null) { diff --git a/src/com/redhat/ci/host/Type.groovy b/src/com/redhat/ci/host/Type.groovy index c6e763c..7347272 100644 --- a/src/com/redhat/ci/host/Type.groovy +++ b/src/com/redhat/ci/host/Type.groovy @@ -7,6 +7,7 @@ package com.redhat.ci.host */ class Type { public static final String BAREMETAL = 'BAREMETAL' - public static final String VM = 'VM' public static final String CONTAINER = 'CONTAINER' + public static final String UNKNOWN = 'UNKNOWN' + public static final String VM = 'VM' } diff --git a/src/com/redhat/ci/hosts/Host.groovy b/src/com/redhat/ci/hosts/Host.groovy index d345192..5cca11d 100644 --- a/src/com/redhat/ci/hosts/Host.groovy +++ b/src/com/redhat/ci/hosts/Host.groovy @@ -15,4 +15,10 @@ class Host { // Host type specification (Baremetal, VM, container) String type = null + + // OS distro + String distro = null + + // OS variant + String variant = null } diff --git a/src/com/redhat/ci/hosts/ProvisionedHost.groovy b/src/com/redhat/ci/hosts/ProvisionedHost.groovy index 3af4485..769ad53 100644 --- a/src/com/redhat/ci/hosts/ProvisionedHost.groovy +++ b/src/com/redhat/ci/hosts/ProvisionedHost.groovy @@ -41,6 +41,8 @@ class ProvisionedHost extends TargetHost { super() this.id = target.id this.arch = target.arch + this.distro = target.distro + this.variant = target.variant this.hostname = target.hostname this.type = target.type this.typePriority = target.typePriority @@ -48,5 +50,11 @@ class ProvisionedHost extends TargetHost { this.providerPriority = target.providerPriority this.provisioner = target.provisioner this.provisionerPriority = target.provisionerPriority + this.bkrHostRequires = target.bkrHostRequires + this.bkrJobGroup = target.bkrJobGroup + this.bkrKsMeta = target.bkrKsMeta + this.bkrMethod = target.bkrMethod + this.reserveDuration = target.reserveDuration + this.scriptParams = target.scriptParams } } diff --git a/src/com/redhat/ci/hosts/TargetHost.groovy b/src/com/redhat/ci/hosts/TargetHost.groovy index 614d1e6..723eade 100644 --- a/src/com/redhat/ci/hosts/TargetHost.groovy +++ b/src/com/redhat/ci/hosts/TargetHost.groovy @@ -4,7 +4,6 @@ package com.redhat.ci.hosts * A target host for provisioning. */ class TargetHost extends Host { - // Host type priority list List typePriority = null @@ -19,4 +18,24 @@ class TargetHost extends Host { // Provisioner type priority list List provisionerPriority = null + + // Beaker hostrequires + // Overrides ProvisioningConfig's hostrequires + List bkrHostRequires = null + + // Beaker jobgroup + // Overrides ProvisioningConfig's jobgroup + String bkrJobGroup = null + + // Beaker ks_meta + String bkrKsMeta = null + + // Beaker installation method + String bkrMethod = null + + // Reservation duration + Integer reserveDuration = null + + // String of parameters to pass to script tests + String scriptParams = null } diff --git a/src/com/redhat/ci/provider/Type.groovy b/src/com/redhat/ci/provider/Type.groovy index 3022d56..809ed88 100644 --- a/src/com/redhat/ci/provider/Type.groovy +++ b/src/com/redhat/ci/provider/Type.groovy @@ -12,4 +12,5 @@ class Type { public static final String KUBEVIRT = 'KUBEVIRT' public static final String OPENSHIFT = 'OPENSHIFT' public static final String OPENSTACK = 'OPENSTACK' + public static final String UNKNOWN = 'UNKNOWN' } diff --git a/src/com/redhat/ci/provisioner/Provisioner.groovy b/src/com/redhat/ci/provisioner/Provisioner.groovy index e0746e0..a7ce4de 100644 --- a/src/com/redhat/ci/provisioner/Provisioner.groovy +++ b/src/com/redhat/ci/provisioner/Provisioner.groovy @@ -11,5 +11,6 @@ interface Provisioner { ProvisionedHost provision(TargetHost target, ProvisioningConfig config) void teardown(ProvisionedHost host, ProvisioningConfig config) Boolean supportsHostType(String hostType) + List filterSupportedHostTypes(List hostTypes) Boolean supportsProvider(String providerType) } diff --git a/src/com/redhat/ci/provisioner/ProvisioningConfig.groovy b/src/com/redhat/ci/provisioner/ProvisioningConfig.groovy index 3c6b135..15b4497 100644 --- a/src/com/redhat/ci/provisioner/ProvisioningConfig.groovy +++ b/src/com/redhat/ci/provisioner/ProvisioningConfig.groovy @@ -13,7 +13,7 @@ class ProvisioningConfig { private static final String JSWARM_EXTRA_ARGS_DEFAULT = '' // Provisioner version - String version = 'v1.2.0' + String version = 'v1.2.1' // Jenkins kubernetes cloud name String cloudName = 'openshift' diff --git a/src/com/redhat/ci/provisioner/ProvisioningService.groovy b/src/com/redhat/ci/provisioner/ProvisioningService.groovy index fdf929a..ec0e19d 100644 --- a/src/com/redhat/ci/provisioner/ProvisioningService.groovy +++ b/src/com/redhat/ci/provisioner/ProvisioningService.groovy @@ -3,6 +3,7 @@ package com.redhat.ci.provisioner import com.redhat.ci.provisioners.LinchPinProvisioner import com.redhat.ci.provisioners.KubeVirtProvisioner import com.redhat.ci.provisioners.OpenShiftProvisioner +import com.redhat.ci.provisioners.NoOpProvisioner import com.redhat.ci.hosts.TargetHost import com.redhat.ci.hosts.ProvisionedHost import com.redhat.ci.host.Type @@ -11,41 +12,50 @@ import com.redhat.ci.host.Type * Utilities for smart provisioning. * Attempts to minimize resource footprint. */ +@SuppressWarnings('AbcMetric') class ProvisioningService { - public static final String UNAVAILABLE = 'No available provisioner could provision target host.' + public static final String UNAVAILABLE = 'No available provisioner could provision target.' @SuppressWarnings('NestedForLoop') - ProvisionedHost provision(TargetHost host, ProvisioningConfig config, Script script) { + ProvisionedHost provision(TargetHost target, ProvisioningConfig config, Script script) { Provisioner provisioner = null + ProvisionedHost host = null - // Users can override the priority list by manually entering their desired host type - if (host.type) { - host.typePriority = [host.type] + // Users can override the priority list by manually entering their desired provisioner type + if (target.provisioner) { + target.provisionerPriority = [target.provisioner] + + // We explicitly set the host type and provider to UNKNOWN to prevent propagation of inaccurate information + // since the NoOpProvisioner doesn't care what kind of host it's provisioning + if (target.provisioner == com.redhat.ci.provisioner.Type.NOOP) { + target.provider = target.provider ?: com.redhat.ci.provider.Type.UNKNOWN + target.type = target.type ?: Type.UNKNOWN + } } - // Users can override the priority list by manually entering their desired provisioner type - if (host.provisioner) { - host.provisionerPriority = [host.provisioner] + // Users can override the priority list by manually entering their desired host type + if (target.type) { + target.typePriority = [target.type] } // Users can override the priority list by manually entering their desired provider type - if (host.provider) { - host.providerPriority = [host.provider] + if (target.provider) { + target.providerPriority = [target.provider] } // Ensure there is a default set for the host type priority - host.typePriority = host.typePriority ?: config.hostTypePriority + target.typePriority = target.typePriority ?: config.hostTypePriority // Ensure there is a default set for the provisioner priority - if (host.provisionerPriority == null) { - host.provisionerPriority = config.provisionerPriority + if (target.provisionerPriority == null) { + target.provisionerPriority = config.provisionerPriority } // Ensure there is a default set for the provider priority - host.providerPriority = host.providerPriority ?: config.providerPriority + target.providerPriority = target.providerPriority ?: config.providerPriority // Loop through each provisioner type by priority - for (provisionerType in host.provisionerPriority) { + for (provisionerType in target.provisionerPriority) { // Verify that there is an available provisioner of this type provisioner = getProvisioner(provisionerType, script) @@ -56,48 +66,54 @@ class ProvisioningService { continue } - // Loop through each host type by priority - for (hostType in host.typePriority) { - // Check if provisioner supports host type - if (!provisioner.supportsHostType(hostType)) { - script.echo("Provisioning ${hostType} host " + - "with ${provisionerType} provisioner is not supported.") + // Filter out each supported host type + List supportedHostTypes = provisioner.filterSupportedHostTypes(target.typePriority) + if (supportedHostTypes.size() == 0) { + script.echo("Provisioning host with ${provisionerType} provisioner cannot be done. " + + "Host types ${target.typePriority} are not supported.") + continue + } + + // Now that we've found a suitable provisioner, let's loop through providers + for (providerType in target.providerPriority) { + // Verify that the selected provisioner supports the selected provider + if (!provisioner.supportsProvider(providerType)) { + script.echo("Provisioning ${target.typePriority} hosts " + + "with ${provisionerType} provisioner " + + "and ${providerType} provider is not supported.") continue } - // Now that we've found a suitable provisioner, let's loop through providers - for (providerType in host.providerPriority) { - // Verify that the selected provisioner supports the selected provider - if (!provisioner.supportsProvider(providerType)) { - script.echo("Provisioning ${hostType} host " + - "with ${provisionerType} provisioner " + - "and ${providerType} provider is not supported.") - continue - } - - // Attempt to provision with the selected provisioner and provider pair - host.provisioner = provisionerType - host.provider = providerType - host.type = hostType - - try { - script.echo("Attempting to provision ${hostType} host " + - "with ${provisionerType} provisioner " + - "and ${providerType} provider.") - return provisioner.provision(host, config) - } catch (e) { - // Provisioning failed, so try next provider - script.echo("Provisioning ${hostType} host " + - "with ${provisionerType} provisioner " + - "and ${providerType} provider failed.") - script.echo("Exception: ${e.message}") - } + // Attempt to provision with the selected provisioner and provider pair + target.provisioner = provisionerType + target.provider = providerType + + script.echo("Attempting to provision ${target.typePriority} host " + + "with ${provisionerType} provisioner " + + "and ${providerType} provider.") + + try { + host = provisioner.provision(target, config) + } catch (e) { + host = new ProvisionedHost(target) + host.error = e.message } + + if (host.error) { + // Provisioning failed, so try next provider + script.echo("Provisioning ${target.typePriority} host " + + "with ${provisionerType} provisioner " + + "and ${providerType} provider failed.") + script.echo("Exception: ${host.error}") + continue + } + + return host } } // If we haven't returned from the function yet, we are out of available - // hostType, provisioner, and provider combinations. + // provisioner and provider combinations for these host type priorities. throw new ProvisioningException(UNAVAILABLE) } @@ -114,6 +130,8 @@ class ProvisioningService { return new OpenShiftProvisioner(script) case com.redhat.ci.provisioner.Type.KUBEVIRT: return new KubeVirtProvisioner(script) + case com.redhat.ci.provisioner.Type.NOOP: + return new NoOpProvisioner(script) default: script.echo("Unrecognized provisioner:${provisioner}") throw new ProvisioningException(UNAVAILABLE) diff --git a/src/com/redhat/ci/provisioner/Type.groovy b/src/com/redhat/ci/provisioner/Type.groovy index 347de1b..c63fc0c 100644 --- a/src/com/redhat/ci/provisioner/Type.groovy +++ b/src/com/redhat/ci/provisioner/Type.groovy @@ -6,7 +6,8 @@ package com.redhat.ci.provisioner * We're not using the Java enum type since it's not supported directly by the groovy security sandbox. */ class Type { - public static final String LINCHPIN = 'LINCHPIN' public static final String KUBEVIRT = 'KUBEVIRT' + public static final String LINCHPIN = 'LINCHPIN' + public static final String NOOP = 'NOOP' public static final String OPENSHIFT = 'OPENSHIFT' } diff --git a/src/com/redhat/ci/provisioners/AbstractProvisioner.groovy b/src/com/redhat/ci/provisioners/AbstractProvisioner.groovy index 6f50ae8..782498c 100644 --- a/src/com/redhat/ci/provisioners/AbstractProvisioner.groovy +++ b/src/com/redhat/ci/provisioners/AbstractProvisioner.groovy @@ -4,11 +4,13 @@ import com.redhat.ci.provisioner.Provisioner import com.redhat.ci.provisioner.ProvisioningConfig import com.redhat.ci.hosts.TargetHost import com.redhat.ci.hosts.ProvisionedHost +import groovy.json.JsonOutput /** * A base provisioner that defines shared utility methods to perform actions upon a provisioned host. */ abstract class AbstractProvisioner implements Provisioner { + public static final String TEARDOWN_NOOP = 'Teardown NoOp' protected static final String ACTIVATE_VIRTUALENV = '. /home/jenkins/envs/provisioner/bin/activate; ' protected static final String PROVISIONING_DIR = 'provisioning' @@ -44,6 +46,17 @@ abstract class AbstractProvisioner implements Provisioner { supportedHostTypes.contains(hostType) } + /** + * Filters a list of host types to those that are supported for provisioning. + */ + @Override + List filterSupportedHostTypes(List hostTypes) { + hostTypes.findAll { + hostType -> + supportsHostType(hostType) + } + } + /** * Determines whether a provider is supported for provisioning. */ @@ -59,4 +72,36 @@ abstract class AbstractProvisioner implements Provisioner { Boolean getAvailable() { this.@available } + + /** + * Injects user credentials to create extra vars needed for Cinch. + */ + protected String getCinchExtraVars(ProvisionedHost host, ProvisioningConfig config) { + script.withCredentials([ + script.usernamePassword(credentialsId:config.jenkinsSlaveCredentialId, + usernameVariable:'JENKINS_SLAVE_USERNAME', + passwordVariable:'JENKINS_SLAVE_PASSWORD'), + ]) { + Map extraVars = [ + 'rpm_key_imports':[], + 'jenkins_master_repositories':[], + 'jenkins_master_download_repositories':[], + 'jslave_name':"${host.displayName}", + 'jslave_label':"${host.displayName}", + 'arch':"${host.arch}", + 'jenkins_master_url':"${config.jenkinsMasterUrl}", + 'jenkins_slave_username':"${script.JENKINS_SLAVE_USERNAME}", + 'jenkins_slave_password':"${script.JENKINS_SLAVE_PASSWORD}", + 'jswarm_version':'3.9', + 'jswarm_filename':'swarm-client-{{ jswarm_version }}.jar', + 'jswarm_extra_args':"${config.jswarmExtraArgs}", + 'jenkins_slave_repositories':[[ + 'name':'epel', + 'mirrorlist':'https://mirrors.fedoraproject.org/metalink?arch=\$basearch&repo=epel-7' + ]] + ] + + JsonOutput.toJson(extraVars) + } + } } diff --git a/src/com/redhat/ci/provisioners/LinchPinProvisioner.groovy b/src/com/redhat/ci/provisioners/LinchPinProvisioner.groovy index bea570d..b1f383b 100644 --- a/src/com/redhat/ci/provisioners/LinchPinProvisioner.groovy +++ b/src/com/redhat/ci/provisioners/LinchPinProvisioner.groovy @@ -1,5 +1,9 @@ package com.redhat.ci.provisioners +import static com.redhat.ci.host.Type.UNKNOWN +import static com.redhat.ci.host.Type.VM +import static com.redhat.ci.host.Type.BAREMETAL + import com.redhat.ci.Utils import com.redhat.ci.hosts.TargetHost import com.redhat.ci.hosts.ProvisionedHost @@ -14,27 +18,43 @@ import groovy.json.JsonOutput */ class LinchPinProvisioner extends AbstractProvisioner { + private static final String HYPERVISOR = 'hypervisor' + private static final Map LINCHPIN_TARGETS = [ (com.redhat.ci.provider.Type.BEAKER):'beaker-slave', ] + private static final Map REQUIRES_VM = [ + tag:HYPERVISOR, op:'!=', value:'', + ] + + private static final Map REQUIRES_BAREMETAL = [ + tag:HYPERVISOR, op:'=', value:'', + ] + LinchPinProvisioner(Script script) { super(script) if (script) { this.available = true } this.type = Type.LINCHPIN - this.supportedHostTypes = [com.redhat.ci.host.Type.VM, com.redhat.ci.host.Type.BAREMETAL] + this.supportedHostTypes = [VM, BAREMETAL] this.supportedProviders = [com.redhat.ci.provider.Type.BEAKER] } + @SuppressWarnings(['AbcMetric', 'UnnecessaryObjectReferences']) ProvisionedHost provision(TargetHost target, ProvisioningConfig config) { ProvisionedHost host = new ProvisionedHost(target) - host.displayName = "${target.arch}-slave" - host.provisioner = this.type - host.provider = com.redhat.ci.provider.Type.BEAKER - try { + // Set the default provisioning values + host.displayName = "${target.arch}-slave" + host.provisioner = this.type + host.provider = com.redhat.ci.provider.Type.BEAKER + host.typePriority = filterSupportedHostTypes(host.typePriority) + host.type = host.typePriority.size() == 1 ? host.typePriority[0] : UNKNOWN + host.distro = host.distro ?: 'RHEL-ALT-7.5' + host.variant = host.variant ?: 'Server' + // Install keys we can connect via JNLP or SSH Utils.installCredentials(script, config) @@ -54,23 +74,29 @@ class LinchPinProvisioner extends AbstractProvisioner { // Attempt provisioning String workspaceDir = "${PROVISIONING_DIR}/${config.provisioningWorkspaceDir}" - script.sh( - ACTIVATE_VIRTUALENV + - "linchpin --workspace ${workspaceDir} " + - "--config ${workspaceDir}/linchpin.conf " + - "--template-data \'${getTemplateData(host, config)}\' " + - "--verbose up ${LINCHPIN_TARGETS[host.provider]}" - ) + try { + script.sh( + ACTIVATE_VIRTUALENV + + "linchpin --workspace ${workspaceDir} " + + "--config ${workspaceDir}/linchpin.conf " + + "--template-data \'${getTemplateData(host, config)}\' " + + "--verbose up ${LINCHPIN_TARGETS[host.provider]}" + ) + } catch (e) { + host.error = e.message + } // Parse the latest run info Map linchpinLatest = script.readJSON(file:"${workspaceDir}/resources/linchpin.latest") // Populate the linchpin transaction ID, inventory path, and hostname host.linchpinTxId = getLinchpinTxId(linchpinLatest) - host.inventoryPath = getLinchpinInventoryPath(linchpinLatest, host) - host.hostname = getHostname(host) script.echo("linchpinTxId:${host.linchpinTxId}") + + host.inventoryPath = getLinchpinInventoryPath(linchpinLatest, host) script.echo("inventoryPath:${host.inventoryPath}") + + host.hostname = getHostname(host) script.echo("hostname:${host.hostname}") // Parse the inventory file for the name of the master node @@ -80,7 +106,7 @@ class LinchPinProvisioner extends AbstractProvisioner { // Run Cinch if in JNLP mode script.sh( ACTIVATE_VIRTUALENV + - "cinch ${host.inventoryPath} --extra-vars='${getExtraVars(host, config)}'") + "cinch ${host.inventoryPath} --extra-vars='${getCinchExtraVars(host, config)}'") host.connectedToMaster = true // In JNLP mode, we can install Ansible so the user can run playbooks @@ -101,8 +127,13 @@ class LinchPinProvisioner extends AbstractProvisioner { Utils.installRhpkg(script, config, host) } } catch (e) { - script.echo("Exception: ${e.message}") - host.error = e.message + host.error = host.error ? host.error + ", ${e.message}" : e.message + script.echo("Error provisioning from LinchPin: ${host.error}") + } + + // An error occured, so we should ensure resources are cleaned up + if (host.error) { + teardown(host, config) } host @@ -116,6 +147,7 @@ class LinchPinProvisioner extends AbstractProvisioner { void teardown(ProvisionedHost host, ProvisioningConfig config) { // Check if the host was even created if (!host) { + script.echo(TEARDOWN_NOOP) return } @@ -127,6 +159,7 @@ class LinchPinProvisioner extends AbstractProvisioner { // The provisioning job did not successfully provision a machine, // so there is nothing to teardown if (!host.initialized) { + script.echo(TEARDOWN_NOOP) return } @@ -156,41 +189,50 @@ class LinchPinProvisioner extends AbstractProvisioner { } private String getTemplateData(ProvisionedHost host, ProvisioningConfig config) { - Map templateData = [:] - templateData.arch = host.arch - templateData.job_group = config.jobgroup - templateData.hostrequires = config.hostrequires + Map templateData = [ + arch:host.arch, + distro:host.distro, + variant:host.variant, + ks_meta:host.bkrKsMeta, + method:host.bkrMethod, + reserve_duration:host.reserveDuration, + job_group:host.bkrJobGroup ?: config.jobgroup, + hostrequires:getHostRequires(host, config), + ] JsonOutput.toJson(templateData) } - private String getExtraVars(ProvisionedHost host, ProvisioningConfig config) { - script.withCredentials([ - script.usernamePassword(credentialsId:config.jenkinsSlaveCredentialId, - usernameVariable:'JENKINS_SLAVE_USERNAME', - passwordVariable:'JENKINS_SLAVE_PASSWORD'), - ]) { - Map extraVars = [ - 'rpm_key_imports':[], - 'jenkins_master_repositories':[], - 'jenkins_master_download_repositories':[], - 'jslave_name':"${host.displayName}", - 'jslave_label':"${host.displayName}", - 'arch':"${host.arch}", - 'jenkins_master_url':"${config.jenkinsMasterUrl}", - 'jenkins_slave_username':"${script.JENKINS_SLAVE_USERNAME}", - 'jenkins_slave_password':"${script.JENKINS_SLAVE_PASSWORD}", - 'jswarm_version':'3.9', - 'jswarm_filename':'swarm-client-{{ jswarm_version }}.jar', - 'jswarm_extra_args':"${config.jswarmExtraArgs}", - 'jenkins_slave_repositories':[[ - 'name':'epel', - 'mirrorlist':'https://mirrors.fedoraproject.org/metalink?arch=\$basearch&repo=epel-7' - ]] - ] - - JsonOutput.toJson(extraVars) + private List getHostRequires(ProvisionedHost host, ProvisioningConfig config) { + List hostrequires = host.bkrHostRequires ?: (config.hostrequires ?: []) + + Closure specifiesHypervisorTag = { + requirement -> + requirement.tag == HYPERVISOR + } + + // If the hypervisor tag is already specified, return default list + if (hostrequires.findAll(specifiesHypervisorTag).size() > 0) { + return hostrequires } + + // If the type priority only allows a single type, add the hypervisor + // hostrequirement manually + if (host.typePriority && host.typePriority.size() == 1) { + switch (host.type) { + case VM: + hostrequires.add(REQUIRES_VM) + break + case BAREMETAL: + hostrequires.add(REQUIRES_BAREMETAL) + break + default: + // Do nothing + break + } + } + + hostrequires } private Integer getLinchpinTxId(Map linchpinLatest) { diff --git a/src/com/redhat/ci/provisioners/NoOpProvisioner.groovy b/src/com/redhat/ci/provisioners/NoOpProvisioner.groovy new file mode 100644 index 0000000..8fe6e8c --- /dev/null +++ b/src/com/redhat/ci/provisioners/NoOpProvisioner.groovy @@ -0,0 +1,188 @@ +package com.redhat.ci.provisioners + +import com.redhat.ci.Utils +import com.redhat.ci.hosts.TargetHost +import com.redhat.ci.hosts.ProvisionedHost +import com.redhat.ci.provisioner.ProvisioningConfig +import com.redhat.ci.provisioner.Mode +import com.redhat.ci.provisioner.Type + +/** + * Emulates a provisioner for a preprovisioned resource. + */ +@SuppressWarnings('AbcMetric') +class NoOpProvisioner extends AbstractProvisioner { + + private static final String PREPROVISIONED_INVENTORY = 'preprovisioned.inventory' + private static final List LAYOUT_GROUPS = [ + 'all', + 'rhel7', + 'certificate_authority', + 'repositories', + 'jenkins_slave', + 'master_node', + ] + + NoOpProvisioner(Script script) { + super(script) + if (script) { + this.available = true + } + this.type = Type.NOOP + this.supportedHostTypes = [ + com.redhat.ci.host.Type.BAREMETAL, + com.redhat.ci.host.Type.CONTAINER, + com.redhat.ci.host.Type.UNKNOWN, + com.redhat.ci.host.Type.VM, + ] + this.supportedProviders = [ + com.redhat.ci.provider.Type.AWS, + com.redhat.ci.provider.Type.BEAKER, + com.redhat.ci.provider.Type.DUFFY, + com.redhat.ci.provider.Type.KUBEVIRT, + com.redhat.ci.provider.Type.OPENSHIFT, + com.redhat.ci.provider.Type.OPENSTACK, + com.redhat.ci.provider.Type.UNKNOWN, + ] + } + + ProvisionedHost provision(TargetHost target, ProvisioningConfig config) { + ProvisionedHost host = new ProvisionedHost(target) + host.displayName = "${target.arch}-slave" + host.provisioner = this.type + + try { + // Install keys we can connect via JNLP or SSH + Utils.installCredentials(script, config) + host.initialized = true + + // A pre-provisioned host must specify the arch + if (!host.arch) { + host.error = 'Arch cannot be null for a pre-provisioned host.' + return host + } + + // A pre-provisioned host must have a hostname + if (!host.hostname) { + host.error = 'Hostname cannot be null for a pre-provisioned host.' + return host + } + + // Build out the inventory if it does not exist + host.inventoryPath = writeInventory(host, config) + if (!host.inventoryPath) { + host.error = 'Inventory could not be found.' + return host + } + + script.echo("inventoryPath:${host.inventoryPath}") + script.echo("hostname:${host.hostname}") + host.provisioned = true + + if (config.mode == Mode.JNLP) { + // Run Cinch if in JNLP mode + script.sh( + ACTIVATE_VIRTUALENV + + "cinch ${host.inventoryPath} --extra-vars='${getCinchExtraVars(host, config)}'") + host.connectedToMaster = true + + // In JNLP mode, we can install Ansible so the user can run playbooks + // (Already installed in SSH mode) + if (config.installAnsible) { + Utils.installAnsible(script, config, host) + } + + // In JNLP mode, install provisioning credentials directly on the provisioned host + // (Already installed in SSH mode) + if (config.installCredentials) { + Utils.installCredentials(script, config, host) + } + } + + // We can install the RHPKG tool if the user intends to use it. + if (config.installRhpkg) { + Utils.installRhpkg(script, config, host) + } + } catch (e) { + script.echo("Exception: ${e.message}") + host.error = e.message + } + + host + } + + /** + * Runs a teardown for provisioned host. + * + * @param host Provisioned host to be torn down. + */ + void teardown(ProvisionedHost host, ProvisioningConfig config) { + // Check if the host was even created + if (!host) { + return + } + + // Host exists, so if there's an error, the job should fail + if (host.error) { + script.currentBuild.result = 'FAILURE' + } + + // The provisioning job did not successfully provision a machine, + // so there is nothing to teardown + if (!host.initialized) { + return + } + + // Run Cinch teardown if we're in JNLP mode and the host was connected to the master node + if (config.mode == Mode.JNLP && host.connectedToMaster) { + try { + script.sh( + ACTIVATE_VIRTUALENV + + "teardown ${host.inventoryPath}" + ) + } catch (e) { + script.echo("Exception: ${e.message}") + } + } + } + + private String writeInventory(ProvisionedHost host, ProvisioningConfig config) { + String inventoryFile = null + try { + // Create inventory filename + String workspaceDir = "${script.pwd()}/${PROVISIONING_DIR}/${config.provisioningWorkspaceDir}" + String newInventoryFile = "${workspaceDir}/inventories/${PREPROVISIONED_INVENTORY}" + + // Get Cinch workspace + if (config.mode == Mode.JNLP) { + script.dir(PROVISIONING_DIR) { + if (config.provisioningRepoUrl != null) { + script.checkout( + scm:[$class:'GitSCM', + userRemoteConfigs:[[url:config.provisioningRepoUrl]], + branches:[[name:config.provisioningRepoRef]]], + poll:false) + } else { + script.checkout(script.scm) + } + } + } + + // Create inventory file, or copy it over if it was passed in + String inventory = '' + + // Build a cinch-compatible inventory using the passing in hostname + for (String group in LAYOUT_GROUPS) { + inventory += "[${group}]\n${host.hostname}\n\n" + } + + // Write and return inventory file path + script.writeFile(file:newInventoryFile, text:inventory) + inventoryFile = newInventoryFile + } catch (e) { + host.error += e.message + } + + inventoryFile + } +} diff --git a/test/PipelineTestScript.groovy b/test/PipelineTestScript.groovy index a2dd0ce..8c95703 100644 --- a/test/PipelineTestScript.groovy +++ b/test/PipelineTestScript.groovy @@ -89,6 +89,11 @@ class PipelineTestScript extends Script { body() } + Closure pwd = { + -> + LOG.info('pwd()') + } + Closure readJSON = { file -> LOG.info("readJSON(${file})") diff --git a/test/TestUtilsTest.groovy b/test/TestUtilsTest.groovy index 9144582..4f9ef89 100644 --- a/test/TestUtilsTest.groovy +++ b/test/TestUtilsTest.groovy @@ -96,7 +96,7 @@ class TestUtilsTest extends PipelineTestScript { @Test void shouldRunTestOnBareMetalHost() { ProvisioningConfig config = TestUtils.getProvisioningConfig(this) - TargetHost target = new TargetHost(arch:X86_64, type:Type.BAREMETAL) + TargetHost target = TestUtils.newTargetHost(arch:X86_64, type:Type.BAREMETAL) TestUtils.runTest(this, target, config, body, onFailure, onComplete) assertNoExceptions() } @@ -104,7 +104,7 @@ class TestUtilsTest extends PipelineTestScript { @Test void shouldRunTestOnVMHost() { ProvisioningConfig config = TestUtils.getProvisioningConfig(this) - TargetHost target = new TargetHost( + TargetHost target = TestUtils.newTargetHost( arch:X86_64, type:Type.VM, provisioner:com.redhat.ci.provisioner.Type.LINCHPIN, @@ -123,7 +123,7 @@ class TestUtilsTest extends PipelineTestScript { config.installAnsible = true config.installRhpkg = true - TargetHost target = new TargetHost(arch:X86_64) + TargetHost target = TestUtils.newTargetHost(arch:X86_64) TestUtils.runTest(this, target, config, body, onFailure, onComplete) assertNoExceptions() reset() @@ -152,7 +152,9 @@ class TestUtilsTest extends PipelineTestScript { void shouldFailWithNoProvisionerAvailable() { ProvisioningConfig config = MAQEAPI.v1.getProvisioningConfig(this) - TargetHost target = new TargetHost(arch:X86_64, provisionerPriority:[]) + TargetHost target = MAQEAPI.v1.newTargetHost() + target.arch = X86_64 + target.provisionerPriority = [] Boolean exceptionOccured = false try { TestUtils.runTest(this, target, config, body, onFailure, onComplete) @@ -163,6 +165,14 @@ class TestUtilsTest extends PipelineTestScript { assert(exceptionOccured) } + @Test + void shouldRunTestOnPreProvisionedHost() { + ProvisioningConfig config = TestUtils.getProvisioningConfig(this) + TargetHost target = TestUtils.newTargetHost(hostname:'test-host.redhat.com', provisioner:'NOOP', arch:X86_64) + TestUtils.runTest(this, target, config, body, onFailure, onComplete) + assertNoExceptions() + } + private void assertNoExceptions() { testLog.each { msg -> diff --git a/test/com/redhat/ci/JobTest.groovy b/test/com/redhat/ci/JobTest.groovy index be03fc3..66efa47 100644 --- a/test/com/redhat/ci/JobTest.groovy +++ b/test/com/redhat/ci/JobTest.groovy @@ -139,12 +139,9 @@ class JobTest extends Job { @Test void teardownNullHost() { - doThrow(new NullPointerException('Null host cannot be torn down.')) - .when(provSvc).teardown(null, config, script) - teardown(null) - verify(provSvc, times(1)).teardown(null, config, script) + verify(provSvc, times(0)).teardown(null, config, script) } @Test @@ -156,6 +153,16 @@ class JobTest extends Job { verify(provSvc, times(1)).teardown(validHost, config, script) } + @Test + void teardownThrowsException() { + doThrow(new NullPointerException('Teardown failed with null value')) + .when(provSvc).teardown(validHost, config, script) + + teardown(validHost) + + verify(provSvc, times(1)).teardown(validHost, config, script) + } + @Test void run() { when(provSvc.provision(target, config, script)).thenReturn(validHost) @@ -173,12 +180,11 @@ class JobTest extends Job { when(provSvc.provision(target, config, script)) .thenThrow(new NullPointerException('Null host cannot provisioned.')) .thenReturn(null) - doNothing().when(provSvc).teardown(any(ProvisionedHost), eq(config), eq(script)) runOnTarget(target) verify(provSvc, times(1)).provision(target, config, script) - verify(provSvc, times(1)).teardown(any(ProvisionedHost), eq(config), eq(script)) + verify(provSvc, times(0)).teardown(any(ProvisionedHost), eq(config), eq(script)) } @Test diff --git a/test/com/redhat/ci/UtilsTest.groovy b/test/com/redhat/ci/UtilsTest.groovy index 49ce23c..5955d86 100644 --- a/test/com/redhat/ci/UtilsTest.groovy +++ b/test/com/redhat/ci/UtilsTest.groovy @@ -132,4 +132,12 @@ class UtilsTest { assert(script.testLog.contains(NODE_STEP)) assert(script.testLog.contains(TEST_HOSTNAME)) } + + @Test + void genericInstallNotARealMode() { + config.mode = 'FAKE' + Utils.genericInstall(script, config, validHost, genericInstall) + assert(!script.testLog.contains(INSTALLED)) + assert(!script.testLog.contains(NODE_STEP)) + } } diff --git a/test/com/redhat/ci/provisioner/ProvisioningServiceTest.groovy b/test/com/redhat/ci/provisioner/ProvisioningServiceTest.groovy index d9e6477..96424b4 100644 --- a/test/com/redhat/ci/provisioner/ProvisioningServiceTest.groovy +++ b/test/com/redhat/ci/provisioner/ProvisioningServiceTest.groovy @@ -6,6 +6,7 @@ import static org.mockito.Mockito.when import static org.mockito.Mockito.thenReturn import static org.mockito.Mockito.thenThrow import static org.mockito.Mockito.anyString +import static org.mockito.Mockito.anyList import static org.mockito.Mockito.verify import static org.mockito.Mockito.times import static com.redhat.ci.provider.Type.BEAKER @@ -24,7 +25,8 @@ import com.redhat.ci.hosts.ProvisionedHost @RunWith(MockitoJUnitRunner) class ProvisioningServiceTest { private static final String EXCEPTION_MESSAGE = 'This is exceptional.' - + private static final String TEST_HOSTNAME = 'test.example.com' + private static final String X86_64 = 'x86_64' private final Script script = new PipelineTestScript() private final ProvisioningConfig config = new ProvisioningConfig() @@ -51,6 +53,29 @@ class ProvisioningServiceTest { assert(script.testLog.contains("Unrecognized provisioner:${target.provisioner}")) } + @Test + void provisioningSkipsProviderNoHostTypesAreSupported() { + TargetHost target = new TargetHost(type:BAREMETAL, provider:BEAKER, provisioner:Type.LINCHPIN) + + Provisioner mockProv = mock(Provisioner) + when(mockProv.available).thenReturn(true) + when(mockProv.supportsProvider(anyString())).thenReturn(false) + when(mockProv.filterSupportedHostTypes(anyList())).thenReturn([]) + when(mockSvc.getProvisioner(target.provisioner, script)).thenReturn(mockProv) + + ProvisionedHost provisionedHost = null + Boolean exceptionOccured = false + try { + provisionedHost = mockSvc.provision(target, config, script) + } catch (e) { + assert(e.message == ProvisioningService.UNAVAILABLE) + exceptionOccured = true + } + + assert(exceptionOccured) + assert(!provisionedHost) + } + @Test void provisionFailureThrowsException() { TargetHost target = new TargetHost(type:BAREMETAL, provider:BEAKER, provisioner:Type.LINCHPIN) @@ -63,7 +88,7 @@ class ProvisioningServiceTest { .thenThrow(new NullPointerException(EXCEPTION_MESSAGE)) when(mockProv.available).thenReturn(true) when(mockProv.supportsProvider(anyString())).thenReturn(true) - when(mockProv.supportsHostType(anyString())).thenReturn(true) + when(mockProv.filterSupportedHostTypes(anyList())).thenReturn([BAREMETAL]) when(mockSvc.getProvisioner(target.provisioner, script)).thenReturn(mockProv) ProvisionedHost provisionedHost = null @@ -78,7 +103,7 @@ class ProvisioningServiceTest { assert(exceptionOccured) assert(!provisionedHost) assert(script.testLog.contains("Exception: ${EXCEPTION_MESSAGE}")) - assert(script.testLog.contains("Provisioning ${target.type} " + + assert(script.testLog.contains("Provisioning ${target.typePriority} " + "host with ${target.provisioner} provisioner " + "and ${target.provider} provider failed.")) } @@ -139,6 +164,34 @@ class ProvisioningServiceTest { verify(mockSvc, times(1)).provision(target, config, script) } + @Test + void noOpProvisionerIsAvailable() { + TargetHost target = new TargetHost(arch:X86_64, provisioner:Type.NOOP, hostname:TEST_HOSTNAME) + + ProvisionedHost host = mockSvc.provision(target, config, script) + + verify(mockSvc, times(1)).provision(target, config, script) + assert(host.provider == com.redhat.ci.provider.Type.UNKNOWN) + assert(host.type == com.redhat.ci.host.Type.UNKNOWN) + } + + @Test + void noOpProvisionerAllowsOverrides() { + TargetHost target = new TargetHost( + arch:X86_64, + hostname:TEST_HOSTNAME, + provisioner:Type.NOOP, + provider:com.redhat.ci.provider.Type.AWS, + type:com.redhat.ci.host.Type.CONTAINER + ) + + ProvisionedHost host = mockSvc.provision(target, config, script) + + verify(mockSvc, times(1)).provision(target, config, script) + assert(host.provider == com.redhat.ci.provider.Type.AWS) + assert(host.type == com.redhat.ci.host.Type.CONTAINER) + } + @Test void defaultTargetProvisionsAndTearsdownSuccessfully() { TargetHost target = new TargetHost() @@ -147,8 +200,8 @@ class ProvisioningServiceTest { Provisioner mockProv = mock(Provisioner) when(mockProv.provision(target, config)).thenReturn(host) when(mockProv.available).thenReturn(true) + when(mockProv.filterSupportedHostTypes(anyList())).thenReturn([BAREMETAL]) when(mockProv.supportsProvider(BEAKER)).thenReturn(true) - when(mockProv.supportsHostType(BAREMETAL)).thenReturn(true) when(mockSvc.getProvisioner(Type.LINCHPIN, script)).thenReturn(mockProv) ProvisionedHost provisionedHost = mockSvc.provision(target, config, script) diff --git a/test/com/redhat/ci/provisioners/LinchPinProvisionerTest.groovy b/test/com/redhat/ci/provisioners/LinchPinProvisionerTest.groovy index 088525e..5282a67 100644 --- a/test/com/redhat/ci/provisioners/LinchPinProvisionerTest.groovy +++ b/test/com/redhat/ci/provisioners/LinchPinProvisionerTest.groovy @@ -37,47 +37,55 @@ class LinchPinProvisionerTest { @Test void testMinimalProvision() { TEST_MODES.each { - mode -> + testMode -> ProvisioningConfig config = new ProvisioningConfig() - config.mode = mode - config.installAnsible = false - config.installCredentials = false - config.installRhpkg = false - config.provisioningRepoUrl = null + config.with { + mode = testMode + installAnsible = false + installCredentials = false + installRhpkg = false + provisioningRepoUrl = null + hostrequires = [[tag:'hostname', value:'test-host', op:'=']] + } ProvisionedHost host = provisioner.provision(new TargetHost(), config) - assert(host) + assert(!host.error) } } @Test void testFullProvision() { TEST_MODES.each { - mode -> + testMode -> ProvisioningConfig config = new ProvisioningConfig() - config.mode = mode - config.installAnsible = true - config.installCredentials = true - config.installRhpkg = true - - ProvisionedHost host = provisioner.provision(new TargetHost(), config) - assert(host) + config.with { + mode = testMode + installAnsible = true + installCredentials = true + installRhpkg = true + } + + ProvisionedHost host = provisioner.provision( + new TargetHost(bkrJobGroup:'maqe', bkrHostRequires:[[tag:'hypervisor', value:'', op:'=']]), + config) + assert(!host.error) } } @Test void testFailedProvision() { Closure sh = { - throw new TestException(EXCEPTION_MESSAGE) + text -> + if (text && text instanceof String && text.contains('linchpin')) { + throw new TestException(EXCEPTION_MESSAGE) + } } script = new PipelineTestScript(sh:sh) provisioner = new LinchPinProvisioner(script) ProvisionedHost host = provisioner.provision(new TargetHost(), new ProvisioningConfig()) - - assert(host) - assert(host.error == EXCEPTION_MESSAGE) + assert(host.error.contains(EXCEPTION_MESSAGE)) } @Test @@ -98,14 +106,14 @@ class LinchPinProvisionerTest { @Test void testTeardown() { provisioner.teardown(new ProvisionedHost(initialized:true), new ProvisioningConfig()) - assert(!script.testLog) + assert(!script.testLog.contains(LinchPinProvisioner.TEARDOWN_NOOP)) } @Test void testTeardownHostIsNullNoOp() { provisioner.teardown(null, new ProvisioningConfig()) assert(script.currentBuild.result == 'SUCCESS') - assert(!script.testLog) + assert(script.testLog.contains(LinchPinProvisioner.TEARDOWN_NOOP)) } @Test @@ -113,7 +121,7 @@ class LinchPinProvisionerTest { provisioner.teardown(new ProvisionedHost(initialized:false, error:EXCEPTION_MESSAGE), new ProvisioningConfig()) assert(script.currentBuild.result == 'FAILURE') - assert(!script.testLog) + assert(script.testLog.contains(LinchPinProvisioner.TEARDOWN_NOOP)) } @Test diff --git a/test/com/redhat/ci/provisioners/NoOpProvisionerTest.groovy b/test/com/redhat/ci/provisioners/NoOpProvisionerTest.groovy new file mode 100644 index 0000000..b34c8cd --- /dev/null +++ b/test/com/redhat/ci/provisioners/NoOpProvisionerTest.groovy @@ -0,0 +1,151 @@ +package com.redhat.ci.provisioners + +import org.junit.Test +import org.junit.Before +import com.redhat.ci.provisioner.ProvisioningConfig +import com.redhat.ci.provisioner.Mode +import com.redhat.ci.hosts.TargetHost +import com.redhat.ci.hosts.ProvisionedHost +import com.redhat.ci.provisioner.Provisioner +import com.redhat.ci.provisioner.ProvisioningException + +/** + * Tests methods belonging to NoOpProvisioner. + */ +class NoOpProvisionerTest { + private static final List TEST_MODES = [Mode.SSH, Mode.JNLP] + private static final String EXCEPTION_MESSAGE = 'An exception occured!' + private static final String TEST_HOSTNAME = 'test-host.redhat.com' + private static final String X86_64 = 'x86_64' + private NoOpProvisioner provisioner + private PipelineTestScript script + + @Before + void init() { + script = new PipelineTestScript() + provisioner = new NoOpProvisioner(script) + } + + @Test + void ensureUnavailableOnlyWhenScriptIsNull() { + Provisioner nullScriptProvisioner = new NoOpProvisioner() + assert(!nullScriptProvisioner.available) + } + + @Test + void ensureAvailableWhenScriptIsNonNull() { + assert(provisioner.available) + } + + @Test + void provisioningFailsWhenMissingArch() { + ProvisioningConfig config = new ProvisioningConfig() + ProvisionedHost host = provisioner.provision(new TargetHost(hostname:TEST_HOSTNAME), config) + assert(host.error) + } + + @Test + void provisioningFailsWhenMissingHostname() { + ProvisioningConfig config = new ProvisioningConfig() + ProvisionedHost host = provisioner.provision(new TargetHost(arch:X86_64), config) + assert(host.error) + } + + @Test + void provisioningFailsWhenMissingInventoryPath() { + ProvisioningConfig config = new ProvisioningConfig() + Closure noWrite = { + Map map -> + throw new ProvisioningException('Error writing file') + } + + script = new PipelineTestScript(writeFile:noWrite) + provisioner = new NoOpProvisioner(script) + + ProvisionedHost host = provisioner.provision(new TargetHost(hostname:TEST_HOSTNAME, arch:X86_64), config) + assert(host.error) + } + + @Test + void testMinimalProvision() { + TEST_MODES.each { + mode -> + ProvisioningConfig config = new ProvisioningConfig() + config.mode = mode + config.installAnsible = false + config.installCredentials = false + config.installRhpkg = false + config.provisioningRepoUrl = null + + ProvisionedHost host = provisioner.provision(new TargetHost(hostname:TEST_HOSTNAME, arch:X86_64), config) + assert(!host.error) + } + } + + @Test + void testFullProvision() { + TEST_MODES.each { + mode -> + ProvisioningConfig config = new ProvisioningConfig() + config.mode = mode + config.installAnsible = true + config.installCredentials = true + config.installRhpkg = true + + ProvisionedHost host = provisioner.provision(new TargetHost(hostname:TEST_HOSTNAME, arch:X86_64), config) + assert(!host.error) + } + } + + @Test + void testFailedProvision() { + Closure sh = { + throw new TestException(EXCEPTION_MESSAGE) + } + + script = new PipelineTestScript(sh:sh) + provisioner = new NoOpProvisioner(script) + + ProvisionedHost host = provisioner.provision(new TargetHost(), new ProvisioningConfig()) + assert(host.error == EXCEPTION_MESSAGE) + } + + @Test + void testTeardown() { + provisioner.teardown(new ProvisionedHost(initialized:true), new ProvisioningConfig()) + assert(!script.testLog) + } + + @Test + void testTeardownHostIsNullNoOp() { + provisioner.teardown(null, new ProvisioningConfig()) + assert(script.currentBuild.result == 'SUCCESS') + assert(!script.testLog) + } + + @Test + void testTeardownHostNoOp() { + provisioner.teardown(new ProvisionedHost(initialized:false, error:EXCEPTION_MESSAGE), + new ProvisioningConfig()) + assert(script.currentBuild.result == 'FAILURE') + assert(!script.testLog) + } + + @Test + void testFailedTeardown() { + Closure sh = { + throw new TestException(EXCEPTION_MESSAGE) + } + + script = new PipelineTestScript(sh:sh) + provisioner = new NoOpProvisioner(script) + + ProvisioningConfig config = new ProvisioningConfig() + config.mode = Mode.JNLP + ProvisionedHost host = new ProvisionedHost(initialized:true, connectedToMaster:true) + + provisioner.teardown(host, config) + + assert(script.testLog.count("Exception: ${EXCEPTION_MESSAGE}") == 1) + } +} diff --git a/vars/TestUtils.groovy b/vars/TestUtils.groovy index 1bdf1e8..093d1a6 100644 --- a/vars/TestUtils.groovy +++ b/vars/TestUtils.groovy @@ -183,4 +183,16 @@ class TestUtils { } } } + + /** + * Returns a new target Host, since shared libraries cannot seem to reference packages are imported in vars. + * See https://issues.jenkins-ci.org/browse/JENKINS-42730 and + * https://stackoverflow.com/questions/51250161/library-jenkins-step-is-not-working-to-dynamically-load-methods?rq=1 + */ + static TargetHost newTargetHost(Map map = null) { + if (!map) { + return new TargetHost() + } + new TargetHost(map) + } } diff --git a/vars/runTests.groovy b/vars/runTests.groovy index 205a3e4..9a382b7 100644 --- a/vars/runTests.groovy +++ b/vars/runTests.groovy @@ -3,40 +3,64 @@ import com.redhat.ci.hosts.ProvisionedHost import com.redhat.ci.provisioner.Mode void call(ProvisioningConfig config, ProvisionedHost host) { + final String ACTIVATE_PROVISIONER = '. /home/jenkins/envs/provisioner/bin/activate;' List exceptions = [] // JNLP Mode if (config.mode == Mode.JNLP) { + // Run Playbooks try { sh "ansible-playbook -i 'localhost,' -c local ${params.TEST_DIR}/ansible-playbooks/*/playbook.yml" } catch (e) { exceptions.add(e) } + // Run tests try { - sh "for i in ${params.TEST_DIR}/scripts/*/test.sh; do bash \$i; done" + sh "for i in ${params.TEST_DIR}/scripts/*/test.sh; do bash \$i ${host.scriptParams}; done" } catch (e) { exceptions.add(e) } + // Collect Artifacts + // Do nothing here since artifacts are already on the host where the archive step will run } // SSH Mode if (config.mode == Mode.SSH) { + // Run Playbooks try { sh(""" - . /home/jenkins/envs/provisioner/bin/activate; - ansible-playbook --key-file '~/.ssh/id_rsa' -i '${host.inventoryPath}' \ - ${params.TEST_DIR}/ansible-playbooks/*/playbook.yml; + ${ACTIVATE_PROVISIONER} + ansible-playbook -i '${host.inventoryPath}' --key-file "~/.ssh/id_rsa" \ + ${params.TEST_DIR}/ansible-playbooks/*/playbook.yml """) } catch (e) { exceptions.add(e) } + // Run Scripts try { + String runScriptsPath = 'run_scripts.yml' + String runScripts = libraryResource(runScriptsPath) + writeFile(file:runScriptsPath, text:runScripts) sh(""" - for i in ${params.TEST_DIR}/scripts/*/test.sh - do ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \ - -i ~/.ssh/id_rsa root@${host.hostname} < \$i - done - """) + ${ACTIVATE_PROVISIONER} + ansible-playbook -i '${host.inventoryPath}' --key-file "~/.ssh/id_rsa" \ + -e '{"test_dir":"${params.TEST_DIR}", "script_params":"${host.scriptParams ?: ''}"}' \ + ${runScriptsPath} + """) + } catch (e) { + exceptions.add(e) + } + // Collect Artifacts + try { + String collectResultsPath = 'collect_results.yml' + String collectResults = libraryResource(collectResultsPath) + writeFile(file:collectResultsPath, text:collectResults) + sh(""" + ${ACTIVATE_PROVISIONER} + ansible-playbook -i '${host.inventoryPath}' --key-file "~/.ssh/id_rsa" \ + -e '{"test_dir":"${params.TEST_DIR}"}' \ + ${collectResultsPath} + """) } catch (e) { exceptions.add(e) } diff --git a/workspace/PinFile b/workspace/PinFile index 73cb2fd..504f770 100644 --- a/workspace/PinFile +++ b/workspace/PinFile @@ -15,9 +15,18 @@ beaker-slave: attempt_wait_time: 60 cancel_message: Beaker request timeout recipesets: - - distro: "RHEL-ALT-7.5" + - distro: {{ distro | default('RHEL-ALT-7.5') }} arch: {{ arch | default('x86_64') }} - variant: Server + variant: {{ variant | default('Server') }} + {% if ks_meta %} + ks_meta: {{ ks_meta | default('') }} + {% endif %} + {% if method %} + method: {{ method | default('nfs') }} + {% endif %} + {% if reserve_duration %} + reserve_duration: {{ reserve_duration | default('86400') }} + {% endif %} count: 1 name: "Jenkins Slave" {% if hostrequires %}