Merge "Revert "Temponarily disable running model tests on hardware nodes""
diff --git a/artifactory-promote-docker-image.groovy b/artifactory-promote-docker-image.groovy
new file mode 100644
index 0000000..e278f05
--- /dev/null
+++ b/artifactory-promote-docker-image.groovy
@@ -0,0 +1,67 @@
+#!groovy
+
+/**
+ *
+ * Promote docker image from one artifactory repository (development) to
+ * another (production)
+ *
+ * Expected parameters:
+ *   REPO_SRC          Source Artifactory repository (default 'docker-dev-local')
+ *   REPO_DST          Destination Artifactory repository (default 'docker-prod-local')
+ *   IMAGE_SRC         Source image name (without docker registry!) to promote (required)
+ *   IMAGE_DST         Destination image (default same as IMAGE_SRC)
+ *
+ *   COPY_IMAGE        Copy image instead of moving (default 'true')
+ *
+ *   ARTIFACTORY_URL   Base URL of Artifactory instance, i.e. without `/api/...` path.
+ *                       (default 'https://artifactory.mcp.mirantis.net/artifactory/')
+ *   ARTIFACTORY_CREDS Credentials to login into Artifactory (default 'artifactory')
+ *
+ *   SLAVE_LABEL       Label of the slave to run job (default 'master')
+ *
+ *   Slave requirements: curl installed
+ *
+ */
+
+import groovy.json.JsonOutput
+
+String repo_src = env.REPO_SRC ?: 'docker-dev-local'
+String repo_dst = env.REPO_DST ?: 'docker-prod-local'
+String image_src = env.IMAGE_SRC
+String image_dst = env.IMAGE_DST ?: env.IMAGE_SRC
+
+boolean copy_image = env.COPY_IMAGE.asBoolean() ?: true
+
+String artifactory_url = env.ARTIFACTORY_URL ?: 'https://artifactory.mcp.mirantis.net/artifactory/'
+String artifactory_creds = env.ARTIFACTORY_CREDS ?: 'artifactory'
+
+String slave_label = env.SLAVE_LABEL ?: 'master'
+
+// Delimiter for splitting docker image name and tag (to avoid codeNarc DRY warning)
+String _colon = ':'
+
+String img_src_name, img_src_tag
+String img_dst_name, img_dst_tag
+
+node(slave_label) {
+    (img_src_name, img_src_tag) = image_src.tokenize(_colon)
+    (img_dst_name, img_dst_tag) = image_dst.tokenize(_colon)
+
+    String api_req = JsonOutput.toJson([
+        targetRepo: repo_dst,
+        dockerRepository: img_src_name,
+        targetDockerRepository: img_dst_name,
+        tag: img_src_tag,
+        targetTag: img_dst_tag,
+        copy: copy_image,
+    ])
+
+    withCredentials([usernameColonPassword(credentialsId: artifactory_creds, variable: 'USERPASS')]) {
+        sh """
+            curl -fLsS \
+                -u \$USERPASS \
+                -X POST -d '${api_req}' -H 'Content-Type: application/json' \
+                '${artifactory_url}api/docker/${repo_src}/v2/promote'
+        """
+    }
+}
diff --git a/build-mirror-image.groovy b/build-mirror-image.groovy
index 8aca89c..33879c0 100644
--- a/build-mirror-image.groovy
+++ b/build-mirror-image.groovy
@@ -46,7 +46,7 @@
         def workspace = common.getWorkspace()
         rcFile = openstack.createOpenstackEnv(OS_URL, OS_CREDENTIALS_ID, OS_PROJECT, "default", "", "default", "2", "")
         openstackEnv = String.format("%s/venv", workspace)
-        def openstackVersion = "ocata"
+        def openstackVersion = OS_VERSION
 
         VM_IP_DELAY = VM_IP_DELAY as Integer
         VM_IP_RETRIES = VM_IP_RETRIES as Integer
@@ -59,7 +59,7 @@
                 sh "mkdir -p ${workspace}/tmp"
             }
 
-            sh "wget https://raw.githubusercontent.com/Mirantis/mcp-common-scripts/master/mirror-image/salt-bootstrap.sh"
+            sh "wget https://raw.githubusercontent.com/Mirantis/mcp-common-scripts/${SCRIPTS_REF}/mirror-image/salt-bootstrap.sh"
             openstack.setupOpenstackVirtualenv(openstackEnv, openstackVersion)
         }
 
@@ -117,7 +117,7 @@
             //NEW way
             //salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.script', ['salt://aptly/files/aptly_mirror_update.sh', "args=-sv", 'runas=aptly'], null, true)
             //salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.script', ['salt://aptly/files/aptly_publish_update.sh', "args=-acrfv", 'runas=aptly'], null, true)
-            salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.run', ['wget https://raw.githubusercontent.com/Mirantis/mcp-common-scripts/master/mirror-image/aptly/aptly-update.sh -O /srv/scripts/aptly-update.sh'], null, true)
+            salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.run', ['wget https://raw.githubusercontent.com/Mirantis/mcp-common-scripts/${SCRIPTS_REF}/mirror-image/aptly/aptly-update.sh -O /srv/scripts/aptly-update.sh'], null, true)
         }
 
         stage("Create Git mirror"){
@@ -128,14 +128,14 @@
         stage("Create PyPi mirror"){
             common.infoMsg("Creating PyPi mirror")
             salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.run', ['pip install pip2pi'], null, true)
-            salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.run', ['wget https://raw.githubusercontent.com/Mirantis/mcp-common-scripts/master/mirror-image/pypi_mirror/requirements.txt -O /srv/pypi_mirror/requirements.txt'], null, true)
+            salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.run', ['wget https://raw.githubusercontent.com/Mirantis/mcp-common-scripts/${SCRIPTS_REF}/mirror-image/pypi_mirror/requirements.txt -O /srv/pypi_mirror/requirements.txt'], null, true)
             salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.run', ['pip2pi /srv/pypi_mirror/packages/ -r /srv/pypi_mirror/requirements.txt'], null, true)
         }
 
         stage("Create mirror of images"){
             common.infoMsg("Creating mirror of images")
-            salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.run', ['wget https://raw.githubusercontent.com/Mirantis/mcp-common-scripts/master/mirror-image/images_mirror/images.txt -O /srv/images.txt'], null, true)
-            salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.run', ['wget https://raw.githubusercontent.com/Mirantis/mcp-common-scripts/master/mirror-image/images_mirror/update-images.sh -O /srv/scripts/update-images.sh'], null, true)
+            salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.run', ['wget https://raw.githubusercontent.com/Mirantis/mcp-common-scripts/${SCRIPTS_REF}/mirror-image/images_mirror/images.txt -O /srv/images.txt'], null, true)
+            salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.run', ['wget https://raw.githubusercontent.com/Mirantis/mcp-common-scripts/${SCRIPTS_REF}/mirror-image/images_mirror/update-images.sh -O /srv/scripts/update-images.sh'], null, true)
             salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.run', ['chmod +x /srv/scripts/update-images.sh'], null, true)
             salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.run', ['/srv/scripts/update-images.sh -u http://ci.mcp.mirantis.net:8085/images'], null, true)
         }
@@ -144,7 +144,9 @@
             salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.run', ['rm -rf /var/lib/cloud/sem/* /var/lib/cloud/instance /var/lib/cloud/instances/*'], null, true)
             salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.run', ['cloud-init init'], null, true)
 
-            openstack.runOpenstackCommand("openstack server stop mcp-offline-mirror-${dateTime}", rcFile, openstackEnv)
+            retry(3, 5){
+                openstack.runOpenstackCommand("openstack server stop mcp-offline-mirror-${dateTime}", rcFile, openstackEnv)
+            }
 
             retry(6, 30){
                 serverStatus = openstack.runOpenstackCommand("openstack server show --format value -c status mcp-offline-mirror-${dateTime}", rcFile, openstackEnv)
@@ -152,12 +154,16 @@
                     throw new ResourceException("Instance is not ready for image create.")
                 }
             }
-            openstack.runOpenstackCommand("openstack server image create --name ${IMAGE_NAME}-${dateTime} --wait mcp-offline-mirror-${dateTime}", rcFile, openstackEnv)
+            retry(3, 5){
+                openstack.runOpenstackCommand("openstack server image create --name ${IMAGE_NAME}-${dateTime} --wait mcp-offline-mirror-${dateTime}", rcFile, openstackEnv)
+            }
         }
 
         stage("Publish image"){
             common.infoMsg("Saving image ${IMAGE_NAME}-${dateTime}")
-            openstack.runOpenstackCommand("openstack image save --file ${IMAGE_NAME}-${dateTime} ${IMAGE_NAME}-${dateTime}", rcFile, openstackEnv)
+            retry(3, 5){
+                openstack.runOpenstackCommand("openstack image save --file ${IMAGE_NAME}-${dateTime} ${IMAGE_NAME}-${dateTime}", rcFile, openstackEnv)
+            }
             sh "md5sum ${IMAGE_NAME}-${dateTime} > ${IMAGE_NAME}-${dateTime}.md5"
 
             common.infoMsg("Uploading image ${IMAGE_NAME}-${dateTime}")
diff --git a/cloud-deploy-pipeline.groovy b/cloud-deploy-pipeline.groovy
index cbb09b7..1b8b5d5 100644
--- a/cloud-deploy-pipeline.groovy
+++ b/cloud-deploy-pipeline.groovy
@@ -400,8 +400,15 @@
 
         }
 
+        if (common.checkContains('STACK_INSTALL', 'oss')) {
+          stage('Install Oss infra') {
+            orchestrate.installOssInfra(venvPepper)
+          }
+        }
+
         if (common.checkContains('STACK_INSTALL', 'cicd')) {
             stage('Install Cicd') {
+                orchestrate.installInfra(venvPepper)
                 orchestrate.installDockerSwarm(venvPepper)
                 orchestrate.installCicd(venvPepper)
             }
@@ -421,6 +428,17 @@
             }
         }
 
+        if (common.checkContains('STACK_INSTALL', 'oss')) {
+          stage('Install OSS') {
+            if (!common.checkContains('STACK_INSTALL', 'stacklight')) {
+              // In case if StackLightv2 enabled containers already started
+              orchestrate.installDockerSwarm(venvPepper)
+              salt.enforceState(venvPepper, 'I@docker:swarm:role:master and I@devops_portal:config', 'docker.client', true)
+            }
+            orchestrate.installOss(venvPepper)
+          }
+        }
+
         //
         // Test
         //
diff --git a/docker-mirror-images.groovy b/docker-mirror-images.groovy
index 91a65a6..8f0373c 100644
--- a/docker-mirror-images.groovy
+++ b/docker-mirror-images.groovy
@@ -22,7 +22,7 @@
         def imageName = matcher.group(1)
         return imageName
     }else{
-        throw new IllegalFormatException("Wrong format of image name.")
+        throw new IllegalArgumentException("Wrong format of image name.")
     }
 }
 
@@ -32,13 +32,18 @@
             def creds = common.getPasswordCredentials(TARGET_REGISTRY_CREDENTIALS_ID)
             sh "docker login --username=${creds.username} --password=${creds.password.toString()} ${REGISTRY_URL}"
             def images = IMAGE_LIST.tokenize('\n')
-            def imageName
+            def imageName, imagePath, targetRegistry, imageArray
             for (image in images){
-                sh "echo ${image}"
-                imageName = getImageName(image)
-                sh "docker pull ${image}"
-                sh "docker tag ${image} ${TARGET_REGISTRY}/${imageName}:${IMAGE_TAG}"
-                sh "docker push ${TARGET_REGISTRY}/${imageName}:${IMAGE_TAG}"
+                if(image.trim().indexOf(' ') == -1){
+                    throw new IllegalArgumentException("Wrong format of image and target repository input")
+                }
+                imageArray = image.trim().tokenize(' ')
+                imagePath = imageArray[0]
+                targetRegistry = imageArray[1]
+                imageName = getImageName(imagePath)
+                sh """docker pull ${imagePath}
+                      docker tag ${imagePath} ${targetRegistry}/${imageName}:${IMAGE_TAG}
+                      docker push ${targetRegistry}/${imageName}:${IMAGE_TAG}"""
             }
         }
     } catch (Throwable e) {
diff --git a/release-mcp-version.groovy b/release-mcp-version.groovy
new file mode 100644
index 0000000..4abaf5d
--- /dev/null
+++ b/release-mcp-version.groovy
@@ -0,0 +1,95 @@
+/**
+ *
+ * Release MCP
+ *
+ * Expected parameters:
+ *   MCP_VERSION
+ *   RELEASE_APTLY
+ *   RELEASE_DOCKER
+ *   RELEASE_GIT
+ *   APTLY_URL
+ *   APTLY_STORAGES
+ *   DOCKER_CREDENTIALS
+ *   DOCKER_URL
+ *   DOCKER_IMAGES
+ *   GIT_CREDENTIALS
+ *   GIT_REPO_LIST
+ */
+
+def common = new com.mirantis.mk.Common()
+
+def triggerAptlyPromoteJob(aptlyUrl, components, diffOnly, dumpPublish, packages, recreate, source, storages, target){
+  build job: "aptly-promote-all-testing-stable", parameters: [
+    [$class: 'StringParameterValue', name: 'APTLY_URL', value: aptlyUrl],
+    [$class: 'StringParameterValue', name: 'COMPONENTS', value: components],
+    [$class: 'BooleanParameterValue', name: 'DIFF_ONLY', value: diffOnly],
+    [$class: 'BooleanParameterValue', name: 'DUMP_PUBLISH', value: dumpPublish],
+    [$class: 'StringParameterValue', name: 'PACKAGES', value: packages],
+    [$class: 'BooleanParameterValue', name: 'RECREATE', value: recreate],
+    [$class: 'StringParameterValue', name: 'SOURCE', value: source],
+    [$class: 'StringParameterValue', name: 'STORAGES', value: storages],
+    [$class: 'StringParameterValue', name: 'TARGET', value: target]
+  ]
+}
+
+def triggerDockerMirrorJob(dockerCredentials, dockerRegistryUrl, dockerRegistry, mcpVersion, imageList) {
+  build job: "mirror-docker-images", parameters: [
+    [$class: 'StringParameterValue', name: 'TARGET_REGISTRY_CREDENTIALS_ID', value: dockerCredentials],
+    [$class: 'StringParameterValue', name: 'REGISTRY_URL', value: dockerRegistryUrl],
+    [$class: 'StringParameterValue', name: 'TARGET_REGISTRY', value: dockerRegistry],
+    [$class: 'StringParameterValue', name: 'IMAGE_TAG', value: mcpVersion],
+    [$class: 'StringParameterValue', name: 'IMAGE_LIST', value: imageList]
+  ]
+}
+
+def gitRepoAddTag(repoURL, repoName, tag, credentials, ref = "HEAD"){
+    git.checkoutGitRepository(repoName, repoURL, "master")
+    dir(repoName) {
+        def checkTag = sh "git tag -l ${tag}"
+        if(checkTag == ""){
+            sh 'git tag -a ${tag} ${ref} -m "Release of mcp version ${tag}"'
+        }
+        sshagent([credentials]) {
+            sh "git push origin master ${tag}"
+        }
+    }
+}
+
+node() {
+    try {
+        if(RELEASE_APTLY.toBoolean())
+        {
+            stage("Release Aptly"){
+                triggerAptlyPromoteJob(APTLY_URL, 'all', false, true, 'all', false, '(.*)/testing', APTLY_STORAGES, '{0}/stable')
+                triggerAptlyPromoteJob(APTLY_URL, 'all', false, true, 'all', false, '(.*)/stable', APTLY_STORAGES, '{0}/${MCP_VERSION}')
+            }
+        }
+        if(RELEASE_DOCKER.toBoolean())
+        {
+            stage("Release Docker"){
+                triggerDockerMirrorJob(DOCKER_CREDENTIALS, DOCKER_URL, MCP_VERSION, DOCKER_IMAGES)
+            }
+        }
+        if(RELEASE_GIT.toBoolean())
+        {
+            stage("Release Git"){
+                def repos = GIT_REPO_LIST.tokenize('\n')
+                def repoUrl, repoName, repoCommit, repoArray
+                for (repo in repos){
+                    if(repo.trim().indexOf(' ') == -1){
+                        throw new IllegalArgumentException("Wrong format of repository and commit input")
+                    }
+                    repoArray = repo.trim().tokenize(' ')
+                    repoName = repoArray[0]
+                    repoUrl = repoArray[1]
+                    repoCommit = repoArray[2]
+                    gitRepoAddTag(repoUrl, repoName, MCP_VERSION, GIT_CREDENTIALS, repoCommit)
+                }
+            }
+        }
+    } catch (Throwable e) {
+        // If there was an error or exception thrown, the build failed
+        currentBuild.result = "FAILURE"
+        throw e
+    }
+}
\ No newline at end of file
diff --git a/update-mirror-image.groovy b/update-mirror-image.groovy
index 0e28a4e..238dbb2 100644
--- a/update-mirror-image.groovy
+++ b/update-mirror-image.groovy
@@ -2,8 +2,19 @@
  * Update mirror image
  *
  * Expected parameters:
- *   SALT_MASTER_CREDENTIALS    Credentials to the Salt API.
- *   SALT_MASTER_URL            Full Salt API address [https://10.10.10.1:8000].
+ *   SALT_MASTER_CREDENTIALS            Credentials to the Salt API.
+ *   SALT_MASTER_URL                    Full Salt API address [https://10.10.10.1:8000].
+ *   UPDATE_APTLY                       Option to update Aptly
+ *   UPDATE_APTLY_MIRRORS               List of mirrors
+ *   PUBLISH_APTLY                      Publish aptly snapshots
+ *   RECREATE_APTLY_PUBLISHES           Option to recreate Aptly publishes separated by comma
+ *   FORCE_OVERWRITE_APTLY_PUBLISHES    Option to force overwrite existing packages while publishing
+ *   CLEANUP_APTLY                      Option to cleanup old Aptly snapshots
+ *   UPDATE_DOCKER_REGISTRY             Option to update Docker Registry
+ *   CLEANUP_DOCKER_CACHE               Option to cleanup locally cached Docker images
+ *   UPDATE_PYPI                        Option to update Python Packages
+ *   UPDATE_GIT                         Option to update Git repositories
+ *   UPDATE_IMAGES                      Option to update VM images
  *
 **/
 
@@ -14,35 +25,75 @@
 
 node() {
     try {
-
         python.setupPepperVirtualenv(venvPepper, SALT_MASTER_URL, SALT_MASTER_CREDENTIALS)
 
-        stage('Update Aptly packages'){
-            common.infoMsg("Updating Aptly packages.")
-            salt.enforceState(venvPepper, 'apt*', ['aptly'], true)
-            salt.runSaltProcessStep(venvPepper, 'apt*', 'cmd.run', ['/srv/scripts/aptly-update.sh'], null, true)
-        }
+        if(UPDATE_APTLY.toBoolean()){
+            stage('Update Aptly mirrors'){
+                def aptlyMirrorArgs = "-s -v"
 
-        stage('Update Docker images'){
-            common.infoMsg("Updating Docker images.")
-            salt.enforceState(venvPepper, 'apt*', ['docker.client.registry'], true)
-        }
+                salt.enforceState(venvPepper, '*apt*', ['aptly.server'], true)
+                sleep(10)
 
-        stage('Update PyPi packages'){
-            common.infoMsg("Updating PyPi packages.")
-            salt.runSaltProcessStep(venvPepper, 'apt*', 'cmd.run', ['pip2pi /srv/pypi_mirror/packages/ -r /srv/pypi_mirror/requirements.txt'], null, true)
+                if(UPDATE_APTLY_MIRRORS != ""){
+                    common.infoMsg("Updating List of Aptly mirrors.")
+                    UPDATE_APTLY_MIRRORS = UPDATE_APTLY_MIRRORS.replaceAll("\\s","")
+                    def mirrors = UPDATE_APTLY_MIRRORS.tokenize(",")
+                    for(mirror in mirrors){
+                        salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.script', ['salt://aptly/files/aptly_mirror_update.sh', "args=\"${aptlyMirrorArgs} -m ${mirror}\"", 'runas=aptly'], null, true)
+                    }
+                }
+                else{
+                    common.infoMsg("Updating all Aptly mirrors.")
+                    salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.script', ['salt://aptly/files/aptly_mirror_update.sh', "args=\"${aptlyMirrorArgs}\"", 'runas=aptly'], null, true)
+                }
+            }
         }
+        if(PUBLISH_APTLY.toBoolean()){
+            def aptlyPublishArgs = "-av"
 
-        stage('Update Git repositories'){
-            common.infoMsg("Updating Git repositories.")
-            salt.enforceState(venvPepper, 'apt*', ['git.server'], true)
+            common.infoMsg("Publishing all Aptly snapshots.")
+
+            salt.enforceState(venvPepper, '*apt*', ['aptly.publisher'], true)
+            sleep(10)
+
+            if(CLEANUP_APTLY.toBoolean()){
+                aptlyPublishArgs += "c"
+            }
+            if(RECREATE_APTLY_PUBLISHES.toBoolean()){
+                aptlyPublishArgs += "r"
+            }
+            if(FORCE_OVERWRITE_APTLY_PUBLISHES.toBoolean()){
+                aptlyPublishArgs += "f"
+            }
+            salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.script', ['salt://aptly/files/aptly_publish_update.sh', "args=\"${aptlyPublishArgs}\"", 'runas=aptly'], null, true)
         }
-
-        stage('Update VM images'){
-            common.infoMsg("Updating VM images.")
-            salt.runSaltProcessStep(venvPepper, 'apt*', 'cmd.run', ['/srv/scripts/update-images.sh'], null, true)
+        if(UPDATE_DOCKER_REGISTRY.toBoolean()){
+            stage('Update Docker images'){
+                common.infoMsg("Updating Docker images.")
+                salt.enforceState(venvPepper, '*apt*', ['docker.client.registry'], true)
+                if(CLEANUP_DOCKER_CACHE.toBoolean()){
+                    salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.run', ['docker system prune --all --force'], null, true)
+                }
+            }
         }
-
+        if(UPDATE_PYPI.toBoolean()){
+            stage('Update PyPi packages'){
+                common.infoMsg("Updating PyPi packages.")
+                salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.run', ['pip2pi /srv/pypi_mirror/packages/ -r /srv/pypi_mirror/requirements.txt'], null, true)
+            }
+        }
+        if(UPDATE_GIT.toBoolean()){
+            stage('Update Git repositories'){
+                common.infoMsg("Updating Git repositories.")
+                salt.enforceState(venvPepper, '*apt*', ['git.server'], true)
+            }
+        }
+        if(UPDATE_IMAGES.toBoolean()){
+            stage('Update VM images'){
+                common.infoMsg("Updating VM images.")
+                salt.runSaltProcessStep(venvPepper, '*apt*', 'cmd.run', ['/srv/scripts/update-images.sh'], null, true)
+            }
+        }
     } catch (Throwable e) {
         // If there was an error or exception thrown, the build failed
         currentBuild.result = "FAILURE"