Merge "Add getBranchesForGitRepo function to Git.groovy class"
diff --git a/src/com/mirantis/mcp/MCPArtifactory.groovy b/src/com/mirantis/mcp/MCPArtifactory.groovy
index de4f81f..b10c1be 100644
--- a/src/com/mirantis/mcp/MCPArtifactory.groovy
+++ b/src/com/mirantis/mcp/MCPArtifactory.groovy
@@ -309,3 +309,68 @@
     }
     sh "rm -v ${queryFile}"
 }
+
+/**
+ * Save job artifacts to Artifactory server if available.
+ * Returns link to Artifactory repo, where saved job artifacts.
+ *
+ * @param config LinkedHashMap which contains next parameters:
+ *   @param artifactory String, Artifactory server id
+ *   @param artifactoryRepo String, repo to save job artifacts
+ *   @param buildProps ArrayList, additional props for saved artifacts. Optional, default: []
+ *   @param artifactory_not_found_fail Boolean, whether to fail if provided artifactory
+ *          id is not found or just print warning message. Optional, default: false
+ */
+def uploadJobArtifactsToArtifactory(LinkedHashMap config) {
+    def common = new com.mirantis.mk.Common()
+    def artifactsDescription = ''
+    def artifactoryServer
+    try {
+        artifactoryServer = Artifactory.server(config.get('artifactory'))
+    } catch (Exception e) {
+        if (config.get('artifactory_not_found_fail', false)) {
+            throw e
+        } else {
+            common.warningMsg(e)
+            return "Artifactory server is not found. Can't save artifacts in Artifactory."
+        }
+    }
+    def artifactDir = 'cur_build_artifacts'
+    def user = ''
+    wrap([$class: 'BuildUser']) {
+        user = env.BUILD_USER_ID
+    }
+    dir(artifactDir) {
+        try {
+            unarchive(mapping: ['**/*' : '.'])
+            // Mandatory and additional properties
+            def properties = getBinaryBuildProperties(config.get('buildProps', []) << "buildUser=${user}")
+
+            // Build Artifactory spec object
+            def uploadSpec = """{
+                "files":
+                    [
+                        {
+                            "pattern": "*",
+                            "target": "${config.get('artifactoryRepo')}/",
+                            "flat": false,
+                            "props": "${properties}"
+                        }
+                    ]
+                }"""
+
+            artifactoryServer.upload(uploadSpec, newBuildInfo())
+            def linkUrl = "${artifactoryServer.getUrl()}/artifactory/${config.get('artifactoryRepo')}"
+            artifactsDescription = "Job artifacts uploaded to Artifactory: <a href=\"${linkUrl}\">${linkUrl}</a>"
+        } catch (Exception e) {
+            if (e =~ /no artifacts/) {
+                artifactsDescription = 'Build has no artifacts saved.'
+            } else {
+                throw e
+            }
+        } finally {
+            deleteDir()
+        }
+    }
+    return artifactsDescription
+}
diff --git a/src/com/mirantis/mcp/Validate.groovy b/src/com/mirantis/mcp/Validate.groovy
index de71cff..79efa08 100644
--- a/src/com/mirantis/mcp/Validate.groovy
+++ b/src/com/mirantis/mcp/Validate.groovy
@@ -417,29 +417,103 @@
  * @param scenarios         Directory inside repo with specific scenarios
  * @param sl_scenarios      Directory inside repo with specific scenarios for stacklight
  * @param tasks_args_file   Argument file that is used for throttling settings
+ * @param db_connection_str Rally-compliant external DB connection string
+ * @param tags              Additional tags used for tagging tasks or building trends
+ * @param trends            Build rally trends if enabled
  * @param ext_variables     The list of external variables
  * @param results           The reports directory
  */
-def runRallyTests(master, target, dockerImageLink, platform, output_dir, config_repo, config_branch, plugins_repo, plugins_branch, scenarios, sl_scenarios = '', tasks_args_file = '', ext_variables = [], results = '/root/qa_results', skip_list = '') {
+def runRallyTests(
+        master, target, dockerImageLink,
+        platform, output_dir, config_repo,
+        config_branch, plugins_repo, plugins_branch,
+        scenarios, sl_scenarios = '', tasks_args_file = '',
+        db_connection_str = '', tags = [],
+        trends = false, ext_variables = [],
+        results = '/root/qa_results', skip_list = ''
+        ) {
+
     def salt = new com.mirantis.mk.Salt()
     def output_file = 'docker-rally.log'
     def dest_folder = '/home/rally/qa_results'
     def env_vars = []
+    def work_dir = 'test_config'
+
+    // compile rally deployment name from env name, platform name,
+    // date, cmp nodes count
+    def deployment_name = ''
+    def cluster_name = salt.getPillar(
+        master, 'I@salt:master', '_param:cluster_name'
+    )['return'][0].values()[0]
+    def rcs_str_node = salt.getPillar(
+        master, 'I@salt:master', 'reclass:storage:node'
+    )['return'][0].values()[0]
+    def date = new Date()
+    date = date.format("yyyyMMddHHmm")
+    def cmp_count
+
+    if (platform['type'] == 'openstack') {
+        cmp_count = rcs_str_node.openstack_compute_rack01['repeat']['count']
+    } else if (platform['type'] == 'k8s') {
+        cmp_count = rcs_str_node.kubernetes_compute_rack01['repeat']['count']
+    } else {
+      throw new Exception("Platform ${platform} is not supported yet")
+    }
+    deployment_name = "env=${cluster_name}:platform=${platform.type}:" +
+        "date=${date}:cmp=${cmp_count}"
+
+    // set up rally cmds
     def rally_extra_args = ''
     def cmd_rally_plugins =
-          "git clone -b ${plugins_branch ?: 'master'} ${plugins_repo} /tmp/plugins; " +
-          "sudo pip install --upgrade /tmp/plugins; "
-    def cmd_rally_init = ''
-    def cmd_rally_checkout = "git clone -b ${config_branch ?: 'master'} ${config_repo} test_config; "
+        "git clone -b ${plugins_branch ?: 'master'} ${plugins_repo} /tmp/plugins; " +
+        "sudo pip install --upgrade /tmp/plugins; "
+    def cmd_rally_init = 'rally db ensure; '
+    if (db_connection_str) {
+        cmd_rally_init = "sudo sed -i -e " +
+            "'s#connection=.*#connection=${db_connection_str}#' " +
+            "/etc/rally/rally.conf; "
+    }
+    def cmd_rally_checkout = "git clone -b ${config_branch ?: 'master'} ${config_repo} ${work_dir}; "
     def cmd_rally_start = ''
     def cmd_rally_task_args = ''
     def cmd_rally_stacklight = ''
-    def cmd_rally_report = ''
+    def cmd_rally_report = "rally task export " +
+        "--uuid \\\$(rally task list --uuids-only --status finished) " +
+        "--type junit-xml --to ${dest_folder}/report-rally.xml; " +
+        "rally task report --uuid \\\$(rally task list --uuids-only --status finished) " +
+        "--out ${dest_folder}/report-rally.html"
+    def cmd_filter_tags = ''
+
+    // build rally trends if required
+    if (trends && db_connection_str) {
+        if (tags) {
+            cmd_filter_tags = "--tag " + tags.join(' ')
+        }
+        cmd_rally_report += "; rally task trends --tasks " +
+            "\\\$(rally task list " + cmd_filter_tags +
+            " --all-deployments --uuids-only --status finished) " +
+            "--out ${dest_folder}/trends-rally.html"
+    }
+
+    // add default env tags for inserting into rally tasks
+    tags = tags + [
+        "env=${cluster_name}",
+        "platform=${platform.type}",
+        "cmp=${cmp_count}"
+    ]
+
+    // create results directory
     salt.runSaltProcessStep(master, target, 'file.remove', ["${results}"])
     salt.runSaltProcessStep(master, target, 'file.mkdir', ["${results}", "mode=777"])
+
+    // get required OS data
     if (platform['type'] == 'openstack') {
-      def _pillar = salt.getPillar(master, 'I@keystone:server', 'keystone')
-      def keystone = _pillar['return'][0].values()[0]
+      cmd_rally_init += "rally deployment create --name='${deployment_name}' --fromenv; " +
+          "rally deployment check; "
+
+      def keystone = salt.getPillar(
+          master, 'I@keystone:server', 'keystone'
+      )['return'][0].values()[0]
       env_vars = ( ['tempest_version=15.0.0',
           "OS_USERNAME=${keystone.server.admin_name}",
           "OS_PASSWORD=${keystone.server.admin_password}",
@@ -448,88 +522,121 @@
           "/v${keystone.client.os_client_config.cfgs.root.content.clouds.admin_identity.identity_api_version}",
           "OS_REGION_NAME=${keystone.server.region}",
           'OS_ENDPOINT_TYPE=admin'] + ext_variables ).join(' -e ')
-      cmd_rally_init = 'rally db create; ' +
-          'rally deployment create --fromenv --name=existing; ' +
-          'rally deployment config; '
+
+      // get required SL data
       if (platform['stacklight_enabled'] == true) {
-        def _pillar_grafana = salt.getPillar(master, 'I@grafana:client', 'grafana:client:server')
-        def grafana = _pillar_grafana['return'][0].values()[0]
-        cmd_rally_stacklight = bundle_up_scenarios(sl_scenarios, skip_list, "scenarios_${platform.type}_stacklight.yaml")
+        def grafana = salt.getPillar(
+            master, 'I@grafana:client', 'grafana:client:server'
+        )['return'][0].values()[0]
+        cmd_rally_stacklight = bundle_up_scenarios(
+            work_dir + '/' + sl_scenarios,
+            skip_list,
+            "scenarios_${platform.type}_stacklight.yaml"
+        )
+        tags.add('stacklight')
         cmd_rally_stacklight += "sed -i 's/grafana_password: .*/grafana_password: ${grafana.password}/' " +
-            "test_config/job-params-stacklight.yaml; " +
-            "rally $rally_extra_args task start scenarios_${platform.type}_stacklight.yaml " +
-            "--task-args-file test_config/job-params-stacklight.yaml; "
+            "${work_dir}/job-params-stacklight.yaml; " +
+            "rally $rally_extra_args task start --tag " + tags.join(' ') +
+            " --task scenarios_${platform.type}_stacklight.yaml " +
+            "--task-args-file ${work_dir}/job-params-stacklight.yaml; "
       }
+
+    // get required K8S data
     } else if (platform['type'] == 'k8s') {
+      cmd_rally_init += "rally env create --name='${deployment_name}' --from-sysenv; " +
+          "rally env check; "
       rally_extra_args = "--debug --log-file ${dest_folder}/task.log"
-      def _pillar = salt.getPillar(master, 'I@kubernetes:master and *01*', 'kubernetes:master')
-      def kubernetes = _pillar['return'][0].values()[0]
+
+      def kubernetes = salt.getPillar(
+          master, 'I@kubernetes:master and *01*', 'kubernetes:master'
+      )['return'][0].values()[0]
       env_vars = [
           "KUBERNETES_HOST=http://${kubernetes.apiserver.vip_address}" +
           ":${kubernetes.apiserver.insecure_port}",
           "KUBERNETES_CERT_AUTH=${dest_folder}/k8s-ca.crt",
           "KUBERNETES_CLIENT_KEY=${dest_folder}/k8s-client.key",
           "KUBERNETES_CLIENT_CERT=${dest_folder}/k8s-client.crt"].join(' -e ')
-      def k8s_ca = salt.getReturnValues(salt.runSaltProcessStep(master,
-                        'I@kubernetes:master and *01*', 'cmd.run',
-                        ["cat /etc/kubernetes/ssl/ca-kubernetes.crt"]))
-      def k8s_client_key = salt.getReturnValues(salt.runSaltProcessStep(master,
-                        'I@kubernetes:master and *01*', 'cmd.run',
-                        ["cat /etc/kubernetes/ssl/kubelet-client.key"]))
-      def k8s_client_crt = salt.getReturnValues(salt.runSaltProcessStep(master,
-                        'I@kubernetes:master and *01*', 'cmd.run',
-                        ["cat /etc/kubernetes/ssl/kubelet-client.crt"]))
+
+      def k8s_ca = salt.getFileContent(
+          master, 'I@kubernetes:master and *01*', '/etc/kubernetes/ssl/ca-kubernetes.crt'
+      )
+      def k8s_client_key = salt.getFileContent(
+          master, 'I@kubernetes:master and *01*', '/etc/kubernetes/ssl/kubelet-client.key'
+      )
+      def k8s_client_crt = salt.getFileContent(
+          master, 'I@kubernetes:master and *01*', '/etc/kubernetes/ssl/kubelet-client.crt'
+      )
       def tmp_dir = '/tmp/kube'
+
       salt.runSaltProcessStep(master, target, 'file.mkdir', ["${tmp_dir}"])
-      salt.runSaltProcessStep(master, target, 'file.write', ["${tmp_dir}/k8s-ca.crt", "${k8s_ca}"])
-      salt.runSaltProcessStep(master, target, 'file.write', ["${tmp_dir}/k8s-client.key", "${k8s_client_key}"])
-      salt.runSaltProcessStep(master, target, 'file.write', ["${tmp_dir}/k8s-client.crt", "${k8s_client_crt}"])
+      salt.runSaltProcessStep(
+          master, target, 'file.write', ["${tmp_dir}/k8s-ca.crt","${k8s_ca}"]
+      )
+      salt.runSaltProcessStep(
+          master, target, 'file.write', ["${tmp_dir}/k8s-client.key", "${k8s_client_key}"]
+      )
+      salt.runSaltProcessStep(
+          master, target, 'file.write', ["${tmp_dir}/k8s-client.crt", "${k8s_client_crt}"]
+      )
       salt.cmdRun(master, target, "mv ${tmp_dir}/* ${results}/")
       salt.runSaltProcessStep(master, target, 'file.rmdir', ["${tmp_dir}"])
-      cmd_rally_init = "rally db recreate; " +
-          "rally env create --name k8s --from-sysenv; " +
-          "rally env check k8s; "
+
     } else {
       throw new Exception("Platform ${platform} is not supported yet")
     }
+
+    // set up rally task args file
     switch(tasks_args_file) {
       case 'none':
         cmd_rally_task_args = '; '
         break
       case '':
-        cmd_rally_task_args = '--task-args-file test_config/job-params-light.yaml; '
+        cmd_rally_task_args = "--task-args-file ${work_dir}/job-params-light.yaml; "
         break
       default:
-        cmd_rally_task_args = "--task-args-file ${tasks_args_file}; "
+        cmd_rally_task_args = "--task-args-file ${work_dir}/${tasks_args_file}; "
       break
     }
     if (platform['type'] == 'k8s') {
-      cmd_rally_start = "for task in \\\$(" + bundle_up_scenarios(scenarios, skip_list) +
+      cmd_rally_start = "for task in \\\$(" +
+          bundle_up_scenarios(work_dir + '/' + scenarios, skip_list) +
           "); do " +
-          "rally $rally_extra_args task start \\\$task ${cmd_rally_task_args}" +
+          "rally $rally_extra_args task start --tag " + tags.join(' ') +
+          " --task \\\$task ${cmd_rally_task_args}" +
           "done; "
     } else {
-      cmd_rally_checkout += bundle_up_scenarios(scenarios, skip_list, "scenarios_${platform.type}.yaml")
-      cmd_rally_start = "rally $rally_extra_args task start " +
-          "scenarios_${platform.type}.yaml ${cmd_rally_task_args}"
+      cmd_rally_checkout += bundle_up_scenarios(
+          work_dir + '/' + scenarios,
+          skip_list,
+          "scenarios_${platform.type}.yaml"
+      )
+      cmd_rally_start = "rally $rally_extra_args task start --tag " + tags.join(' ') +
+          " --task scenarios_${platform.type}.yaml ${cmd_rally_task_args}"
     }
-    cmd_rally_report= "rally task export --uuid \\\$(rally task list --uuids-only --status finished) " +
-        "--type junit-xml --to ${dest_folder}/report-rally.xml; " +
-        "rally task report --uuid \\\$(rally task list --uuids-only --status finished) " +
-        "--out ${dest_folder}/report-rally.html"
 
+    // compile full rally cmd
     full_cmd = 'set -xe; ' + cmd_rally_plugins +
         cmd_rally_init + cmd_rally_checkout +
         'set +e; ' + cmd_rally_start +
-        cmd_rally_stacklight +
-        cmd_rally_report
+        cmd_rally_stacklight + cmd_rally_report
+
+    // run rally
     salt.runSaltProcessStep(master, target, 'file.touch', ["${results}/rally.db"])
     salt.cmdRun(master, target, "chmod 666 ${results}/rally.db")
-    salt.cmdRun(master, target, "docker run -w /home/rally -i --rm --net=host -e ${env_vars} " +
+    salt.cmdRun(
+        master, target,
+        "docker run -w /home/rally -i --rm --net=host -e ${env_vars} " +
         "-v ${results}:${dest_folder} " +
         "-v ${results}/rally.db:/home/rally/data/rally.db " +
         "--entrypoint /bin/bash ${dockerImageLink} " +
         "-c \"${full_cmd}\" > ${results}/${output_file}")
+
+    // remove k8s secrets
+    if (platform['type'] == 'k8s') {
+        salt.cmdRun(master, target, "rm ${results}/k8s-*")
+    }
+
+    // save artifacts
     addFiles(master, target, results, output_dir)
 }
 
diff --git a/src/com/mirantis/mk/Common.groovy b/src/com/mirantis/mk/Common.groovy
index 1cb3d6e..d0f3598 100644
--- a/src/com/mirantis/mk/Common.groovy
+++ b/src/com/mirantis/mk/Common.groovy
@@ -730,7 +730,9 @@
                     description += "<li><a href=\"${httpWS}/old/${item}/*view*/\">${item}</a></li>"
                 }
             }
-
+            def cwd = sh(script: 'basename $(pwd)', returnStdout: true).trim()
+            sh "tar -cf old_${cwd}.tar.gz old/ && rm -rf old/"
+            sh "tar -cf new_${cwd}.tar.gz new/ && rm -rf new/"
         }
     }
 
diff --git a/src/com/mirantis/mk/Galera.groovy b/src/com/mirantis/mk/Galera.groovy
index d48cfc3..5a15823 100644
--- a/src/com/mirantis/mk/Galera.groovy
+++ b/src/com/mirantis/mk/Galera.groovy
@@ -17,7 +17,9 @@
  */
 
 def getWsrepParameters(env, target, parameters=[], print=false) {
-    result = []
+    def salt = new com.mirantis.mk.Salt()
+    def common = new com.mirantis.mk.Common()
+    result = [:]
     out = salt.runSaltProcessStep(env, "${target}", "mysql.status", [], null, false)
     outlist = out['return'][0]
     resultYaml = outlist.get(outlist.keySet()[0]).sort()
@@ -30,12 +32,12 @@
     if (parameters == [] || parameters == ['']) {
         result = resultYaml
     } else {
-        for (key in parameters) {
-            value = resultYaml[key]
+        for (String param in parameters) {
+            value = resultYaml[param]
             if (value instanceof String && value.isBigDecimal()) {
                 value = value.toBigDecimal()
             }
-            result = ["${key}": value] + result
+            result[param] = value
         }
     }
     return result
@@ -217,13 +219,27 @@
     }
 }
 
-def getGaleraLastShutdownNode(env) {
+/** Returns last shutdown node of Galera cluster
+@param env      Salt Connection object or pepperEnv
+@param nodes    List of nodes to check only (defaults to []). If not provided, it will check all nodes.
+                Use this parameter if the cluster splits to several components and you only want to check one fo them.
+@return status  ip address or hostname of last shutdown node
+*/
+
+def getGaleraLastShutdownNode(env, nodes = []) {
     def salt = new com.mirantis.mk.Salt()
     def common = new com.mirantis.mk.Common()
-    members = ''
+    members = []
     lastNode = [ip: '', seqno: -2]
     try {
-        members = salt.getReturnValues(salt.getPillar(env, "I@galera:master", "galera:master:members"))
+        if (nodes) {
+            nodes = salt.getIPAddressesForNodenames(env, nodes)
+            for (node in nodes) {
+                members = [host: "${node.get(node.keySet()[0])}"] + members
+            }
+        } else {
+            members = salt.getReturnValues(salt.getPillar(env, "I@galera:master", "galera:master:members"))
+        }
     } catch (Exception er) {
         common.errorMsg('Could not retrieve members list')
         return 'I@galera:master'
diff --git a/src/com/mirantis/mk/Openstack.groovy b/src/com/mirantis/mk/Openstack.groovy
index 29edec9..9ce6aef 100644
--- a/src/com/mirantis/mk/Openstack.groovy
+++ b/src/com/mirantis/mk/Openstack.groovy
@@ -435,26 +435,40 @@
 /**
  * Return intersection of globally installed services and those are
  * defined on specific target according to theirs priorities.
+ * By default services are added to the result list only if
+ * <service>.upgrade.enabled pillar is set to "True". However if it
+ * is needed to obtain list of upgrade services regardless of
+ * <service>.upgrade.enabled pillar value it is needed to set
+ * "upgrade_condition" param to "False".
  *
  * @param env     Salt Connection object or env
- * @param target  The target node to get list of apps for.
+ * @param target  The target node to get list of apps for
+ * @param upgrade_condition  Whether to take "upgrade:enabled"
+ *                           service pillar into consideration
+ *                           when obtaining list of upgrade services
 **/
-def getOpenStackUpgradeServices(env, target){
+def getOpenStackUpgradeServices(env, target, upgrade_condition=true){
     def salt = new com.mirantis.mk.Salt()
     def common = new com.mirantis.mk.Common()
 
     def global_apps = salt.getConfig(env, 'I@salt:master:enabled:true', 'orchestration.upgrade.applications')
     def node_apps = salt.getPillar(env, target, '__reclass__:applications')['return'][0].values()[0]
-    def node_pillar = salt.getPillar(env, target)
+    if (upgrade_condition) {
+        node_pillar = salt.getPillar(env, target)
+    }
     def node_sorted_apps = []
     if ( !global_apps['return'][0].values()[0].isEmpty() ) {
         Map<String,Integer> _sorted_apps = [:]
         for (k in global_apps['return'][0].values()[0].keySet()) {
             if (k in node_apps) {
-                if (node_pillar['return'][0].values()[k]['upgrade']['enabled'][0] != null) {
-                    if (node_pillar['return'][0].values()[k]['upgrade']['enabled'][0].toBoolean()) {
-                      _sorted_apps[k] = global_apps['return'][0].values()[0][k].values()[0].toInteger()
+                if (upgrade_condition) {
+                    if (node_pillar['return'][0].values()[k]['upgrade']['enabled'][0] != null) {
+                        if (node_pillar['return'][0].values()[k]['upgrade']['enabled'][0].toBoolean()) {
+                            _sorted_apps[k] = global_apps['return'][0].values()[0][k].values()[0].toInteger()
+                        }
                     }
+                } else {
+                    _sorted_apps[k] = global_apps['return'][0].values()[0][k].values()[0].toInteger()
                 }
             }
         }
@@ -467,7 +481,6 @@
   return node_sorted_apps
 }
 
-
 /**
  * Run specified upgrade phase for all services on given node.
  *
@@ -536,4 +549,4 @@
     def galera = new com.mirantis.mk.Galera()
     common.warningMsg("restoreGaleraDb method was moved to Galera class. Please change your calls accordingly.")
     return galera.restoreGaleraDb(env)
-}
\ No newline at end of file
+}
diff --git a/src/com/mirantis/mk/Salt.groovy b/src/com/mirantis/mk/Salt.groovy
index b1ae4a3..7688c0e 100644
--- a/src/com/mirantis/mk/Salt.groovy
+++ b/src/com/mirantis/mk/Salt.groovy
@@ -1196,3 +1196,33 @@
         return false
     }
 }
+
+/**
+* Finds out IP address of the given node or a list of nodes
+*
+* @param saltId     Salt Connection object or pepperEnv (the command will be sent using the selected method)
+* @param nodes      Targeted node hostnames to be checked (String or List of strings)
+* @param useGrains  If the, the value will be taken from grains. If false, it will be taken from 'hostname' command.
+* @return Map       Return result Map in format ['nodeName1': 'ipAdress1', 'nodeName2': 'ipAdress2', ...]
+*/
+
+def getIPAddressesForNodenames(saltId, nodes = [], useGrains = true) {
+    result = [:]
+
+    if (nodes instanceof String) {
+        nodes = [nodes]
+    }
+
+    if (useGrains) {
+        for (String node in nodes) {
+            ip = getReturnValues(getGrain(saltId, node, "fqdn_ip4"))["fqdn_ip4"][0]
+            result[node] = ip
+        }
+    } else {
+        for (String node in nodes) {
+            ip = getReturnValues(cmdRun(saltId, node, "hostname -i")).readLines()[0]
+            result[node] = ip
+        }
+    }
+    return result
+}
\ No newline at end of file