Merge "Add [vsphere-demo] on demand trigger"
diff --git a/src/com/mirantis/mcp/Git.groovy b/src/com/mirantis/mcp/Git.groovy
index 92359a0..4231016 100644
--- a/src/com/mirantis/mcp/Git.groovy
+++ b/src/com/mirantis/mcp/Git.groovy
@@ -79,6 +79,9 @@
   def credentialsId = config.get('credentialsId', '')
   def protocol = config.get('protocol', 'ssh')
   def refspec = config.get('refspec', null)
+  String branch = config.get('branch', 'FETCH_HEAD')
+  Integer depth = config.get('depth', 0)
+  Integer timeout = config.get('timeout', 0)
 
   // default parameters
   def scmExtensions = [
@@ -88,7 +91,7 @@
 
   // https://issues.jenkins-ci.org/browse/JENKINS-6856
   if (merge) {
-    scmExtensions.add([$class: 'LocalBranch', localBranch: "${config.branch}"])
+    scmExtensions.add([$class: 'LocalBranch', localBranch: "${branch}"])
   }
 
   // we need wipe workspace before checkout
@@ -96,10 +99,20 @@
     scmExtensions.add([$class: 'WipeWorkspace'])
   }
 
+  // optionally limit depth of checkout
+  if (depth) {
+    scmExtensions.add([$class: 'CloneOption', depth: "${depth}", shallow: 'true'])
+  }
+
+  // optionally set timeout
+  if (timeout) {
+    scmExtensions.add([$class: 'CloneOption', timeout: "${timeout}"])
+  }
+
   checkout(
     scm: [
       $class: 'GitSCM',
-      branches: [[name: "${config.branch}"]],
+      branches: [[name: "${branch}"]],
       extensions: scmExtensions,
       userRemoteConfigs: [[
         credentialsId: credentialsId,
diff --git a/src/com/mirantis/mk/Lock.groovy b/src/com/mirantis/mk/Lock.groovy
new file mode 100644
index 0000000..10efa71
--- /dev/null
+++ b/src/com/mirantis/mk/Lock.groovy
@@ -0,0 +1,146 @@
+import org.jfrog.hudson.pipeline.common.types.ArtifactoryServer
+import java.util.concurrent.TimeoutException
+
+class Lock {
+    String  name, id, path
+    Integer retryInterval, timeout, expiration
+    Boolean force
+    Map lockExtraMetadata
+    ArtifactoryServer artifactoryServer
+
+    private String lockFileContent
+    private String lockFileContentCache
+
+    final private String fileUri
+
+    final private def common = new com.mirantis.mk.Common()
+    final private def artifactoryTools = new com.mirantis.mk.Artifactory()
+
+    // Constructor
+    public Lock(Map args) {
+        // Mandatory
+        this.name = args.name
+        this.artifactoryServer = args.artifactoryServer
+
+        // Defaults
+        this.id = args.get('id', '')
+        this.path = args.get('path', 'binary-dev-local/locks')
+        this.retryInterval = args.get('retryInterval', 5*60)
+        this.timeout = args.get('timeout', 3*60*60)
+        this.expiration = args.get('expiration', 24*60*60)
+        this.force = args.get('force', false)
+        this.lockExtraMetadata = args.get('lockExtraMetadata', [:])
+
+        // Internal
+        this.fileUri = "/${path}/${name}.yaml".toLowerCase()
+    }
+
+    final private Map artObj
+    // getPasswordCredentials() is CPS-transformed function and cannot be used in constructor
+    final private Map getArtObj() {
+        def artifactoryCreds = common.getPasswordCredentials(artifactoryServer.getCredentialsId())
+        return [
+            'url': "${artifactoryServer.getUrl()}/artifactory",
+            'creds': [
+                'username': artifactoryCreds['username'],
+                'password': artifactoryCreds['password'],
+            ]
+        ]
+    }
+
+    // getter for lockFileContent
+    final private String getLockFileContent() {
+        if (this.lockFileContentCache == null) {
+            try {
+                this.lockFileContentCache = artifactoryTools.restCall(this.artObj, this.fileUri, 'GET', null, [:], '')
+            } catch (Exception e) {
+                this.lockFileContentCache = ''
+            }
+        }
+        return this.lockFileContentCache
+    }
+
+    public void lock() {
+        if (this.force) {
+            common.infoMsg("Ignore lock checking due 'force' flag presence")
+        } else {
+            waitLockReleased()
+        }
+        createLockFile()
+    }
+
+    public void unlock() {
+        if (!isLockFileExist()) {
+            common.infoMsg("Lock file '${this.artObj['url']}${this.fileUri}' does not exist. No need to remove it")
+            // No need to continue if file does not exist
+            return
+        }
+
+        Map lockMeta = common.readYaml2(text: this.lockFileContent ?: '{}')
+        if (this.force || (this.id && this.id == lockMeta.get('lockID', ''))) {
+            artifactoryTools.restCall(this.artObj, this.fileUri, 'DELETE', null, [:], '')
+            common.infoMsg("Lock file '${this.artObj['url']}${this.fileUri}' has been removed")
+        } else {
+            throw new RuntimeException("Given lock ID '${this.id}' is not equal to '${lockMeta.get('lockID')}' ID in lock file")
+        }
+    }
+
+    private void createLockFile() {
+        this.id = UUID.randomUUID().toString()
+
+        Calendar now = Calendar.getInstance()
+        Calendar expiredAt = now.clone()
+        expiredAt.add(Calendar.SECOND, this.expiration)
+
+        Map lockMeta = [
+            'lockID': this.id,
+            'createdAt': now.getTime().toString(),
+            'expiredAt': expiredAt.getTime().toString(),
+        ]
+        lockMeta.putAll(this.lockExtraMetadata)
+
+        def commonMCP = new com.mirantis.mcp.Common()
+        artifactoryTools.restCall(this.artObj, this.fileUri, 'PUT', commonMCP.dumpYAML(lockMeta), [:], '')
+        common.infoMsg("Lock file '${this.artObj['url']}${this.fileUri}' has been created")
+    }
+
+    private void waitLockReleased() {
+        Long startTime = System.currentTimeMillis()
+        while (isLocked()) {
+            if (System.currentTimeMillis() - startTime >= timeout*1000 ) {
+                throw new TimeoutException("Execution of waitLock timed out after ${this.timeout} seconds")
+            }
+            common.infoMsg("'${this.name}' is locked. Retry in ${this.retryInterval} seconds")
+            // Reset the cache so it will re-retrieve the file and its content
+            // otherwise it cannot determine that file has been removed on artifactory
+            // in the middle of waiting
+            this.lockFileContentCache = null
+            sleep(this.retryInterval*1000)
+        }
+    }
+
+    private Boolean isLocked() {
+        if (!isLockFileExist()) {
+            common.infoMsg("Lock file for '${this.name}' does not exist")
+            return false
+        } else if (isLockExpired()) {
+            common.infoMsg("Lock '${this.name}' has been expired")
+            return false
+        }
+        return true
+    }
+
+    private Boolean isLockFileExist() {
+        return !this.lockFileContent.isEmpty()
+    }
+
+    private Boolean isLockExpired() {
+        if (!isLockFileExist()) {
+            return true
+        }
+        Map lockMeta = common.readYaml2(text: this.lockFileContent ?: '{}')
+        Date expirationTime = new Date(lockMeta.get('expiredAt', '01/01/1970'))
+        Date currentTime = new Date()
+        return currentTime.after(expirationTime)
+    }
+}
diff --git a/src/com/mirantis/mk/Orchestrate.groovy b/src/com/mirantis/mk/Orchestrate.groovy
index e762eed..de8ce68 100644
--- a/src/com/mirantis/mk/Orchestrate.groovy
+++ b/src/com/mirantis/mk/Orchestrate.groovy
@@ -83,6 +83,8 @@
     salt.enforceState([saltId: master, target: "I@salt:master ${extra_tgt}", state: ['reclass'], failOnError: false, read_timeout: 120, retries: 2])
     salt.fullRefresh(master, target)
     salt.enforceState([saltId: master, target: target, state: ['linux.network.proxy'], failOnError: false, read_timeout: 60, retries: 2])
+    // Make sure all repositories are in place before proceeding with package installation from other states
+    salt.enforceState([saltId: master, target: target, state: ['linux.system.repo'], batch: batch, failOnError: false, read_timeout: 180, retries: 2])
     try {
         salt.enforceState([saltId: master, target: target, state: ['salt.minion.base'], failOnError: false, read_timeout: 60, retries: 2])
         sleep(5)