Add new function for running Salt/Reclass in Docker

Related-bug: PROD-22115 (PROD:22115)

Change-Id: Ibbba9cfb87b9221f17e4df8e524128ce46fd82e6
diff --git a/src/com/mirantis/mk/SaltModelTesting.groovy b/src/com/mirantis/mk/SaltModelTesting.groovy
index 381247a..ba1a993 100644
--- a/src/com/mirantis/mk/SaltModelTesting.groovy
+++ b/src/com/mirantis/mk/SaltModelTesting.groovy
@@ -1,6 +1,219 @@
 package com.mirantis.mk
 
 /**
+ * Setup Docker to run some tests. Returns true/false based on
+ were tests successful or not.
+ * @param config - LinkedHashMap with configuration params:
+ *   dockerHostname - (required) Hostname to use for Docker container.
+ *   formulasRevision - (optional) Revision of packages to use (default stable).
+ *   runCommands - (optional) Dict with closure structure of body required tests. For example:
+ *     [ '001_Test': { sh("./run-some-test") }, '002_Test': { sh("./run-another-test") } ]
+ *     Before execution runCommands will be sorted by key names. Alpabetical order is preferred.
+ *   runFinally - (optional) Dict with closure structure of body required commands, which should be
+ *     executed in any case of test results. Same format as for runCommands
+ *   updateRepo - (optional) Whether to run common repo update step.
+ *   dockerContainerName - (optional) Docker container name.
+ *   dockerImageName - (optional) Docker image name
+ *   dockerMaxCpus - (optional) Number of CPUS to use in Docker.
+ *   dockerExtraOpts - (optional) Array of Docker extra opts for container
+ *   envOpts - (optional) Array of variables that should be passed as ENV vars to Docker container.
+ * Return true | false
+ */
+
+def setupDockerAndTest(LinkedHashMap config) {
+    def common = new com.mirantis.mk.Common()
+    def TestMarkerResult = false
+    // setup options
+    def defaultContainerName = 'test-' + UUID.randomUUID().toString()
+    def dockerHostname = config.get('dockerHostname', defaultContainerName)
+    def formulasRevision = config.get('formulasRevision', 'proposed')
+    def runCommands = config.get('runCommands', [:])
+    def runFinally = config.get('runFinally', [:])
+    def baseRepoPreConfig = config.get('baseRepoPreConfig', true)
+    def dockerContainerName = config.get('dockerContainerName', defaultContainerName)
+    def dockerImageName = config.get('image', "mirantis/salt:saltstack-ubuntu-xenial-salt-2017.7")
+    def dockerMaxCpus = config.get('dockerMaxCpus', 4)
+    def dockerExtraOpts = config.get('dockerExtraOpts', [])
+    def envOpts = config.get('envOpts', [])
+    envOpts.add("DISTRIB_REVISION=${formulasRevision}")
+    def dockerBaseOpts = [
+        '-u root:root',
+        "--hostname=${dockerHostname}",
+        '--ulimit nofile=4096:8192',
+        "--name=${dockerContainerName}",
+        "--cpus=${dockerMaxCpus}"
+    ]
+
+    def dockerOptsFinal = (dockerBaseOpts + dockerExtraOpts).join(' ')
+    def img = docker.image(dockerImageName)
+    img.pull()
+
+    try {
+        img.inside(dockerOptsFinal) {
+            withEnv(envOpts) {
+                try {
+                    // Currently, we don't have any other point to install
+                    // runtime dependencies for tests.
+                    if (baseRepoPreConfig) {
+                        sh("""#!/bin/bash -xe
+                            echo "Installing extra-deb dependencies inside docker:"
+                            echo "APT::Get::AllowUnauthenticated 'true';"  > /etc/apt/apt.conf.d/99setupAndTestNode
+                            echo "APT::Get::Install-Suggests 'false';"  >> /etc/apt/apt.conf.d/99setupAndTestNode
+                            echo "APT::Get::Install-Recommends 'false';"  >> /etc/apt/apt.conf.d/99setupAndTestNode
+                            rm -vf /etc/apt/sources.list.d/* || true
+                            echo 'deb [arch=amd64] http://mirror.mirantis.com/$DISTRIB_REVISION/ubuntu xenial main restricted universe' > /etc/apt/sources.list
+                            echo 'deb [arch=amd64] http://mirror.mirantis.com/$DISTRIB_REVISION/ubuntu xenial-updates main restricted universe' >> /etc/apt/sources.list
+                            apt-get update
+                            apt-get install -y python-netaddr
+                        """)
+                    }
+                    runCommands.sort().each { command, body ->
+                        common.warningMsg("Running command: ${command}")
+                        // doCall is the closure implementation in groovy, allow to pass arguments to closure
+                        body.call()
+                    }
+                    // If we didn't dropped for now - test has been passed.
+                    TestMarkerResult = true
+                }
+                finally {
+                    runFinally.sort().each { command, body ->
+                        common.warningMsg("Running ${command} command.")
+                        // doCall is the closure implementation in groovy, allow to pass arguments to closure
+                        body.call()
+                    }
+                }
+            }
+        }
+    }
+    catch (Exception er) {
+        common.warningMsg("IgnoreMe:Something wrong with img.Message:\n" + er.toString())
+    }
+
+    try {
+        common.warningMsg("IgnoreMe:Force cleanup slave.Ignore docker-daemon errors")
+        timeout(time: 10, unit: 'SECONDS') {
+            sh(script: "set -x; docker kill ${dockerContainerName} || true", returnStdout: true)
+        }
+        timeout(time: 10, unit: 'SECONDS') {
+            sh(script: "set -x; docker rm --force ${dockerContainerName} || true", returnStdout: true)
+        }
+    }
+    catch (Exception er) {
+        common.warningMsg("IgnoreMe:Timeout to delete test docker container with force!Message:\n" + er.toString())
+    }
+
+    if (TestMarkerResult) {
+        common.infoMsg("Test finished: SUCCESS")
+    } else {
+        common.warningMsg("Test finished: FAILURE")
+    }
+    return TestMarkerResult
+}
+
+/**
+ * Wrapper over setupDockerAndTest, to test CC model.
+ *
+ * @param config - dict with params:
+ *   dockerHostname - (required) salt master's name
+ *   clusterName - (optional) model cluster name
+ *   extraFormulas - (optional) extraFormulas to install. DEPRECATED
+ *   formulasSource - (optional) formulas source (git or pkg, default pkg)
+ *   reclassVersion - (optional) Version of used reclass (branch, tag, ...) (optional, default master)
+ *   reclassEnv - (require) directory of model
+ *   ignoreClassNotfound - (optional) Ignore missing classes for reclass model (default false)
+ *   aptRepoUrl - (optional) package repository with salt formulas
+ *   aptRepoGPG - (optional) GPG key for apt repository with formulas
+ *   testContext - (optional) Description of test
+  Return: true\exception
+ */
+
+def testNode(LinkedHashMap config) {
+    def common = new com.mirantis.mk.Common()
+    def result = ''
+    def dockerHostname = config.get('dockerHostname')
+    def reclassEnv = config.get('reclassEnv')
+    def clusterName = config.get('clusterName', "")
+    def formulasSource = config.get('formulasSource', 'pkg')
+    def extraFormulas = config.get('extraFormulas', 'linux')
+    def reclassVersion = config.get('reclassVersion', 'master')
+    def ignoreClassNotfound = config.get('ignoreClassNotfound', false)
+    def aptRepoUrl = config.get('aptRepoUrl', "")
+    def aptRepoGPG = config.get('aptRepoGPG', "")
+    def testContext = config.get('testContext', 'test')
+    config['envOpts'] = [
+        "RECLASS_ENV=${reclassEnv}", "SALT_STOPSTART_WAIT=5",
+        "MASTER_HOSTNAME=${dockerHostname}", "CLUSTER_NAME=${clusterName}",
+        "MINION_ID=${dockerHostname}", "FORMULAS_SOURCE=${formulasSource}",
+        "EXTRA_FORMULAS=${extraFormulas}", "RECLASS_VERSION=${reclassVersion}",
+        "RECLASS_IGNORE_CLASS_NOTFOUND=${ignoreClassNotfound}", "DEBUG=1",
+        "APT_REPOSITORY=${aptRepoUrl}", "APT_REPOSITORY_GPG=${aptRepoGPG}",
+        "EXTRA_FORMULAS_PKG_ALL=true"
+    ]
+
+    config['runCommands'] = [
+        '001_Clone_salt_formulas_scripts': {
+          sh(script: 'git clone https://github.com/salt-formulas/salt-formulas-scripts /srv/salt/scripts', returnStdout: true)
+        },
+
+        '002_Prepare_something': {
+            sh('''rsync -ah ${RECLASS_ENV}/* /srv/salt/reclass && echo '127.0.1.2  salt' >> /etc/hosts
+              cd /srv/salt && find . -type f \\( -name '*.yml' -or -name '*.sh' \\) -exec sed -i 's/apt-mk.mirantis.com/apt.mirantis.net:8085/g' {} \\;
+              cd /srv/salt && find . -type f \\( -name '*.yml' -or -name '*.sh' \\) -exec sed -i 's/apt.mirantis.com/apt.mirantis.net:8085/g' {} \\;
+            ''')
+        },
+
+        // should be switched on packages later
+        '003_Install_reclass': {
+            sh('''for s in \$(python -c \"import site; print(' '.join(site.getsitepackages()))\"); do
+              sudo -H pip install --install-option=\"--prefix=\" --upgrade --force-reinstall -I \
+              -t \"\$s\" git+https://github.com/salt-formulas/reclass.git@${RECLASS_VERSION};
+              done
+            ''')
+        },
+
+        '004_Run_tests': {
+            def testTimeout = 40 * 60
+            timeout(time: testTimeout, unit: 'SECONDS') {
+              sh('''#!/bin/bash
+                source /srv/salt/scripts/bootstrap.sh
+                cd /srv/salt/scripts
+                source_local_envs
+                configure_salt_master
+                configure_salt_minion
+                install_salt_formula_pkg
+                source /srv/salt/scripts/bootstrap.sh
+                cd /srv/salt/scripts
+                saltservice_restart''')
+
+              sh('''#!/bin/bash
+                source /srv/salt/scripts/bootstrap.sh
+                cd /srv/salt/scripts
+                source_local_envs
+                saltmaster_init''')
+
+              sh('''#!/bin/bash
+                source /srv/salt/scripts/bootstrap.sh
+                cd /srv/salt/scripts
+                verify_salt_minions''')
+            }
+        }
+    ]
+    config['runFinally'] = [
+        '001_Archive_artefacts': {
+            sh(script: "cd /tmp; tar -czf ${env.WORKSPACE}/nodesinfo.tar.gz *reclass*", returnStatus: true)
+            archiveArtifacts artifacts: "nodesinfo.tar.gz"
+        }
+    ]
+    testResult = setupDockerAndTest(config)
+    if (testResult) {
+        common.infoMsg("Node test for context: ${testContext} model: ${reclassEnv} finished: SUCCESS")
+    } else {
+        throw new RuntimeException("Node test for context: ${testContext} model: ${reclassEnv} finished: FAILURE")
+    }
+    return testResult
+}
+
+/**
  * setup and test salt-master
  *
  * @param masterName salt master's name