Merge "[CVP,master] Fix cvp-perf job for xrally 0.11.2"
diff --git a/src/com/mirantis/mk/Artifactory.groovy b/src/com/mirantis/mk/Artifactory.groovy
index d1eb218..73ed80f 100644
--- a/src/com/mirantis/mk/Artifactory.groovy
+++ b/src/com/mirantis/mk/Artifactory.groovy
@@ -394,3 +394,66 @@
         )
     }
 }
+
+/**
+ * Get Helm repo for Artifactory
+ *
+ * @param art           Artifactory connection object
+ * @param repoName      Chart repository name
+ */
+def getArtifactoryProjectByName(art, repoName){
+    return restGet(art, "/repositories/${repoName}")
+}
+
+/**
+ * Get repo by packageType for Artifactory
+ *
+ * @param art           Artifactory connection object
+ * @param packageType   Repository package type
+ */
+def getArtifactoryProjectByPackageType(art, repoName){
+    return restGet(art, "/repositories?${packageType}")
+}
+
+/**
+ * Create Helm repo for Artifactory
+ *
+ * @param art           Artifactory connection object
+ * @param repoName      Chart repository name
+ * @param data          Transmitted data
+ */
+def createArtifactoryChartRepo(art, repoName){
+    return restPut(art, "/repositories/${repoName}", '{"rclass": "local","handleSnapshots": false,"packageType": "helm"}')
+}
+
+/**
+ * Delete Helm repo for Artifactory
+ *
+ * @param art           Artifactory connection object
+ * @param repoName      Chart repository name
+ */
+def deleteArtifactoryChartRepo(art, repoName){
+    return restDelete(art, "/repositories/${repoName}")
+}
+
+/**
+ * Create Helm repo for Artifactory
+ *
+ * @param art           Artifactory connection object
+ * @param repoName      Repository Chart name
+ * @param chartName     Chart name
+ */
+def publishArtifactoryHelmChart(art, repoName, chartName){
+    return restPut(art, "/repositories/${repoName}", "${chartName}")
+}
+
+/**
+ * Create Helm repo for Artifactory
+ *
+ * @param art           Artifactory connection object
+ * @param repoName      Repository Chart name
+ * @param chartName     Chart name
+ */
+def deleteArtifactoryHelmChart(art, repoName, chartName){
+    return restDelete(art, "/repositories/${repoName}", "${chartName}")
+}
diff --git a/src/com/mirantis/mk/Common.groovy b/src/com/mirantis/mk/Common.groovy
index b5760da..c0059bb 100644
--- a/src/com/mirantis/mk/Common.groovy
+++ b/src/com/mirantis/mk/Common.groovy
@@ -460,10 +460,14 @@
     }
     else if (overrides.length == 1) {
         overrides[0]?.each { k, v ->
-            if (v in Map && onto[k] in Map){
-                mergeMaps((Map) onto[k], (Map) v)
-            } else if (v in List) {
-                onto[k] += v
+            if (k in onto.keySet()) {
+                if (v in Map && onto[k] in Map){
+                    mergeMaps((Map) onto[k], (Map) v)
+                } else if (v in List) {
+                    onto[k] += v
+                } else {
+                    onto[k] = v
+                }
             } else {
                 onto[k] = v
             }
@@ -1006,3 +1010,16 @@
 Date parseDate(String date, String format) {
     return Date.parse(format, date)
 }
+
+/**
+ * Generate Random Hash string
+ * @param n Hash length
+ * @param pool Pool to use for hash generation
+*/
+def generateRandomHashString(int n, ArrayList pool = []) {
+    if (!pool) {
+        pool = ['a'..'z','A'..'Z',0..9,'_','+','='].flatten()
+    }
+    Random rand = new Random(System.currentTimeMillis())
+    return (1..n).collect { pool[rand.nextInt(pool.size())] }.join()
+}
diff --git a/src/com/mirantis/mk/Galera.groovy b/src/com/mirantis/mk/Galera.groovy
index 733914c..382a72f 100644
--- a/src/com/mirantis/mk/Galera.groovy
+++ b/src/com/mirantis/mk/Galera.groovy
@@ -105,8 +105,8 @@
             }
             for (int i = 0; i < iostatRes.size(); i++) {
                 def diskKey = iostatRes.keySet()[i]
-                if (!(iostatRes[diskKey].toString().isBigDecimal() && (iostatRes[diskKey].toBigDecimal() < 0.5 ))) {
-                    common.errorMsg("Disk ${diskKey} has to high i/o utilization. Maximum value is 0.5 and current value is ${iostatRes[diskKey]}.")
+                if (!(iostatRes[diskKey].toString().isBigDecimal() && (iostatRes[diskKey].toBigDecimal() < 50 ))) {
+                    common.errorMsg("Disk ${diskKey} has to high i/o utilization. Maximum value is 50 and current value is ${iostatRes[diskKey]}.")
                     return 141
                 }
             }
@@ -347,23 +347,38 @@
         common.warningMsg('File is not present')
     }
 
+    // make sure that gcom parameter is empty
     salt.cmdRun(env, lastNodeTarget, "sed -i '/gcomm/c\\wsrep_cluster_address=\"gcomm://\"' /etc/mysql/my.cnf")
 
+    // run restore of DB
     if (runRestoreDb) {
         restoreGaleraDb(env, lastNodeTarget)
     }
 
-    salt.enforceState(env, lastNodeTarget, 'galera')
+    // start mysql service on the last node
+    salt.runSaltProcessStep(env, lastNodeTarget, 'service.start', ['mysql'])
 
-    // wait until mysql service on galera master is up
+    // wait until mysql service on the last node is up
     try {
         salt.commandStatus(env, lastNodeTarget, 'service mysql status', 'running')
     } catch (Exception er) {
         input message: "Database is not running please fix it first and only then click on PROCEED."
     }
 
+    // start mysql services on the rest of the nodes
     salt.runSaltProcessStep(env, "I@galera:master and not ${lastNodeTarget}", 'service.start', ['mysql'])
     salt.runSaltProcessStep(env, "I@galera:slave and not ${lastNodeTarget}", 'service.start', ['mysql'])
+
+    // wait until mysql service on the rest of the nodes is up
+    try {
+        salt.commandStatus(env, "( I@galera:master or I@galera:slave ) and not ${lastNodeTarget}", 'service mysql status', 'running')
+    } catch (Exception er) {
+        input message: "Database is not running please fix it first and only then click on PROCEED."
+    }
+
+    // apply any changes in configuration
+    salt.enforceState(env, lastNodeTarget, 'galera')
+
 }
 
 /**
diff --git a/src/com/mirantis/mk/GoogleCloudStorage.groovy b/src/com/mirantis/mk/GoogleCloudStorage.groovy
new file mode 100644
index 0000000..5bf5ad7
--- /dev/null
+++ b/src/com/mirantis/mk/GoogleCloudStorage.groovy
@@ -0,0 +1,126 @@
+package com.mirantis.mk
+
+/**
+    Work with Google Cloud Storage
+**/
+
+/** Exists or not gcloud binary file
+ *
+ * @param gcloudBinDir Path to check
+*/
+def checkGcloudBinary(String gcloudBinDir) {
+    def status = sh(script: "${gcloudBinDir}/google-cloud-sdk/bin/gcloud info > /dev/null", returnStatus: true)
+    return "${status}" == "0"
+}
+
+/** Download gcloud archive with binarties
+ *
+ * @param gcloudBinDir Path to save binaries
+ * @param url Specific URL to use to download
+*/
+def downloadGcloudUtil(String gcloudBinDir, String url="") {
+    if (!url) {
+        url="https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-253.0.0-linux-x86_64.tar.gz"
+    }
+    dir(gcloudBinDir) {
+        def archiveName='google-cloud-sdk'
+        sh """
+            wget -O ${archiveName}.tar.gz ${url}
+            tar -xf ${archiveName}.tar.gz
+        """
+    }
+}
+
+/** Auth gcloud util with provided creds
+ *
+ * @param gcloudBinDir Path to save binaries
+ * @param creds Creds name to use, saved as secret file
+ * @param projectName Project name to use
+*/
+def authGcloud(String gcloudBinDir, String creds, String projectName) {
+    ws {
+        withCredentials([
+            file(credentialsId: creds,
+                variable: 'key_file')
+        ]) {
+            sh "${gcloudBinDir}/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file $key_file --project ${projectName}"
+        }
+    }
+}
+
+/** Revoke gcloud auth account
+ *
+ * @param gcloudBinDir Path to save binaries
+*/
+def revokeGcloud(String gcloudBinDir) {
+    sh "${gcloudBinDir}/google-cloud-sdk/bin/gcloud auth revoke"
+}
+
+/** Copy file to Google Storage Bucket
+ *
+ * @param gcloudBinDir Path to save binaries
+ * @param src Source file
+ * @param dst Destination path in storage
+ * @param entireTree Copy entire directory tree
+*/
+def cpFile(String gcloudBinDir, String src, String dst, Boolean entireTree=false) {
+    def fileURL = ''
+    if (entireTree) {
+        sh "${gcloudBinDir}/google-cloud-sdk/bin/gsutil cp -r ${src} ${dst}"
+        return dst
+    } else {
+        def fileBaseName = sh(script:"basename ${src}", returnStdout: true).trim()
+        sh "${gcloudBinDir}/google-cloud-sdk/bin/gsutil cp ${src} ${dst}/${fileBaseName}"
+        return "${dst}/${fileBaseName}"
+    }
+}
+
+/** Set ACL on files in bucket
+ *
+ * @param gcloudBinDir Path to save binaries
+ * @param path Path to file in bucket
+ * @param acls ACLs to be set for file
+*/
+def setAcl(String gcloudBinDir, String path, ArrayList acls) {
+    for(String acl in acls) {
+        sh "${gcloudBinDir}/google-cloud-sdk/bin/gsutil acl ch -u ${acl} ${path}"
+    }
+}
+
+/** Upload files to Google Cloud Storage Bucket
+ *
+ * @param config LinkedHashMap with next parameters:
+ *   @param gcloudBinDir Path to save binaries
+ *   @param creds Creds name to use, saved as secret file
+ *   @param projectName Project name to use
+ *   @param sources List of file to upload
+ *   @param dest Destination path in Google Storage, in format: gs://<path>
+ *   @param acls ACLs for uploaded files
+ *   @param entireTree Copy entire directory to bucket
+ *
+ * Returns URLs list of uploaded files
+*/
+def uploadArtifactToGoogleStorageBucket(Map config) {
+    def gcloudDir = config.get('gcloudDir', '/tmp/gcloud')
+    def creds = config.get('creds')
+    def project = config.get('project')
+    def acls = config.get('acls', ['AllUsers:R'])
+    def sources = config.get('sources')
+    def dest = config.get('dest')
+    def entireTree = config.get('entireTree', false)
+    def fileURLs = []
+    if (!checkGcloudBinary(gcloudDir)) {
+        downloadGcloudUtil(gcloudDir)
+    }
+    try {
+        authGcloud(gcloudDir, creds, project)
+        for(String src in sources) {
+            def fileURL = cpFile(gcloudDir, src, dest, entireTree)
+            setAcl(gcloudDir, fileURL, acls)
+            fileURLs << fileURL
+        }
+    } finally {
+        revokeGcloud(gcloudDir)
+    }
+    return fileURLs
+}
\ No newline at end of file
diff --git a/src/com/mirantis/mk/Salt.groovy b/src/com/mirantis/mk/Salt.groovy
index 21bb4c9..daff9fc 100644
--- a/src/com/mirantis/mk/Salt.groovy
+++ b/src/com/mirantis/mk/Salt.groovy
@@ -458,32 +458,45 @@
  * You can call this function when salt-master already contains salt keys of the target_nodes
  * @param saltId Salt Connection object or pepperEnv (the command will be sent using the selected method)
  * @param target Should always be salt-master
- * @param target_nodes unique identification of a minion or group of salt minions
+ * @param targetNodes unique identification of a minion or group of salt minions
  * @param batch salt batch parameter integer or string with percents (optional, default null - disable batch)
- * @param wait timeout for the salt command if minions do not return (default 10)
+ * @param cmdTimeout timeout for the salt command if minions do not return (default 10)
  * @param maxRetries finite number of iterations to check status of a command (default 200)
  * @return output of salt command
  */
-def minionsReachable(saltId, target, target_nodes, batch=null, wait = 10, maxRetries = 200) {
+
+def minionsReachable(saltId, target, targetNodes, batch=null, cmdTimeout = 10, maxRetries = 200) {
     def common = new com.mirantis.mk.Common()
-    def cmd = "salt -t${wait} -C '${target_nodes}' test.ping"
-    common.infoMsg("Checking if all ${target_nodes} minions are reachable")
-    def count = 0
-    while(count < maxRetries) {
+    def cmd = "salt -t${cmdTimeout} -C '${targetNodes}' test.ping"
+    common.infoMsg("Checking if all ${targetNodes} minions are reachable")
+    def retriesCount = 0
+    while(retriesCount < maxRetries) {
         Calendar timeout = Calendar.getInstance();
-        timeout.add(Calendar.SECOND, wait);
-        def out = runSaltCommand(saltId, 'local', ['expression': target, 'type': 'compound'], 'cmd.shell', batch, [cmd], null, wait)
+        timeout.add(Calendar.SECOND, cmdTimeout);
+        def out = runSaltCommand(saltId, 'local', ['expression': target, 'type': 'compound'], 'cmd.shell', batch, [cmd], null, cmdTimeout)
         Calendar current = Calendar.getInstance();
         if (current.getTime().before(timeout.getTime())) {
-           printSaltCommandResult(out)
-           return out
+            common.infoMsg("Successful response received from all targeted nodes.")
+            printSaltCommandResult(out)
+            return out
         }
-        common.infoMsg("Not all of the targeted '${target_nodes}' minions returned yet. Waiting ...")
-        count++
+        def outYaml = readYaml text: getReturnValues(out)
+        def successfulNodes = []
+        def failedNodes = []
+        for (node in outYaml.keySet()) {
+            if (outYaml[node] == true || outYaml[node].toString().toLowerCase() == 'true') {
+                successfulNodes.add(node)
+            } else {
+                failedNodes.add(node)
+            }
+        }
+        common.infoMsg("Not all of the targeted minions returned yet. Successful response from ${successfulNodes}. Still waiting for ${failedNodes}.")
+        retriesCount++
         sleep(time: 500, unit: 'MILLISECONDS')
     }
 }
 
+
 /**
  * You can call this function when need to check that all minions are available, free and ready for command execution
  * @param config LinkedHashMap config parameter, which contains next:
@@ -518,6 +531,42 @@
 }
 
 /**
+ * Restart and wait for salt-minions on target nodes.
+ * @param saltId Salt Connection object or pepperEnv (the command will be sent using the selected method)
+ * @param target unique identification of a minion or group of salt minions
+ * @param wait timeout for the salt command if minions do not return (default 5)
+ * @param maxRetries finite number of iterations to check status of a command (default 10)
+ * @return output of salt command
+ */
+def restartSaltMinion(saltId, target, wait = 5, maxRetries = 10) {
+    def common = new com.mirantis.mk.Common()
+    common.infoMsg("Restarting salt-minion on ${target} and waiting for they are reachable.")
+    runSaltProcessStep(saltId, target, 'cmd.shell', ['salt-call service.restart salt-minion'], null, true, 60)
+    checkTargetMinionsReady(['saltId': saltId, 'target_reachable': target, timeout: wait, retries: maxRetries])
+    common.infoMsg("All ${target} minions are alive...")
+}
+
+/**
+ * Upgrade package and restart salt minion.
+ * @param saltId Salt Connection object or pepperEnv (the command will be sent using the selected method)
+ * @param target unique identification of a minion or group of salt minions
+ * @param the name of pkg_name to upgrade
+ * @param wait timeout for the salt command if minions do not return (default 5)
+ * @param maxRetries finite number of iterations to check status of a command (default 10)
+ * @return output of salt command
+ */
+def upgradePackageAndRestartSaltMinion(saltId, target, pkg_name, wait = 5, maxRetries = 10) {
+    def common = new com.mirantis.mk.Common()
+    def latest_version = getReturnValues(runSaltProcessStep(saltId, target, 'pkg.latest_version', [pkg_name, 'show_installed=True'])).split('\n')[0]
+    def current_version = getReturnValues(runSaltProcessStep(saltId, target, 'pkg.version', [pkg_name])).split('\n')[0]
+    if (current_version && latest_version != current_version) {
+        common.infoMsg("Upgrading current ${pkg_name}: ${current_version} to ${latest_version}")
+        runSaltProcessStep(saltId, target, 'pkg.install', [pkg_name], 'only_upgrade=True')
+        restartSaltMinion(saltId, target, wait, maxRetries)
+    }
+}
+
+/**
  * Run command on salt minion (salt cmd.run wrapper)
  * @param saltId Salt Connection object or pepperEnv (the command will be sent using the selected method)
  * @param target Get pillar target