From 1227d47a63632c0d2b6f6a2a192a2975252d6569 Mon Sep 17 00:00:00 2001 From: Uladzislau Date: Tue, 3 Sep 2024 16:44:25 +0200 Subject: [PATCH 1/4] Transition to JDK 17 Signed-off-by: Uladzislau --- .github/setup/action.yml | 2 +- .github/workflows/hpi-builder.yml | 6 +++--- Jenkinsfile | 2 +- pom.xml | 30 ++++++++++++++++++++---------- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/.github/setup/action.yml b/.github/setup/action.yml index 0d39adb..802090c 100644 --- a/.github/setup/action.yml +++ b/.github/setup/action.yml @@ -5,7 +5,7 @@ inputs: jdkVersion: description: "JDK version" required: false - default: "11" + default: "17" mavenVersion: description: "Maven version" required: false diff --git a/.github/workflows/hpi-builder.yml b/.github/workflows/hpi-builder.yml index 987e892..7c5ff09 100644 --- a/.github/workflows/hpi-builder.yml +++ b/.github/workflows/hpi-builder.yml @@ -20,7 +20,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '11' + java-version: '17' cache: maven - name: Build Jenkins Plugin into .hpi @@ -52,9 +52,9 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '11' + java-version: '17' cache: maven - name: Run Unit Tests with Maven shell: bash - run: ./mvnw test \ No newline at end of file + run: ./mvnw test diff --git a/Jenkinsfile b/Jenkinsfile index 87d1157..12f6a8d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -8,4 +8,4 @@ * Copyright IBA Group 2022 */ -buildPlugin(useContainerAgent: true, tests: [[skip: 'true']], configurations: [[ platform: 'linux', jdk: '11' ]]) \ No newline at end of file +buildPlugin(useContainerAgent: true, tests: [[skip: 'true']], configurations: [[ platform: 'linux', jdk: '17' ]]) diff --git a/pom.xml b/pom.xml index f14afc6..f507f17 100644 --- a/pom.xml +++ b/pom.xml @@ -22,18 +22,28 @@ 0.2.0- -SNAPSHOT 2.414.3 - 11 - 1.8.21 + 17 + 1.9.20 true 1.13 4.10.0 - 0.5.0-rc.1 + 0.5.0-rc.11 5.6.1 official - 11 + 17 UTF-8 High jenkinsci/${project.artifactId}-plugin + + UTF-8 + zowe + zowe_zowe-zdevops-jenkins-plugin + zowe-zdevops-jenkins-plugin + project.version + kotlin + https://github.com/zowe/zowe-zdevops-jenkins-plugin + + 17 @@ -133,10 +143,10 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.13.0 - 11 - 11 + 17 + 17 @@ -302,19 +312,19 @@ com.squareup.retrofit2 retrofit - 2.9.0 + 2.11.0 com.squareup.retrofit2 converter-gson - 2.9.0 + 2.11.0 com.squareup.retrofit2 converter-scalars - 2.9.0 + 2.11.0 From db232f49078c3be5b805ceeddca1e34ad1094cfd Mon Sep 17 00:00:00 2001 From: Anatoli Kalbasin <37359814+callbacksin@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:35:00 +0300 Subject: [PATCH 2/4] IJMP-1875 - failOnExist options (#84) * feat: implement failOnExist option Signed-off-by: Anatoli Kalbasin * fix: pass failOnNotExist default values Signed-off-by: Anatoli Kalbasin * clarify URL template hint Signed-off-by: Anatoli Kalbasin --- .../classic/steps/AllocateDatasetStep.kt | 4 +- .../classic/steps/DeleteDatasetStep.kt | 12 ++-- .../classic/steps/DeleteDatasetsByMaskStep.kt | 3 +- .../zdevops/classic/steps/SubmitJobStep.kt | 4 +- .../jobs/AllocateDatasetDeclarative.kt | 6 +- .../jobs/DeleteDatasetDeclarative.kt | 6 +- .../jobs/DeleteDatasetsByMaskDeclarative.kt | 6 +- .../zowe/zdevops/logic/AllocateOperation.kt | 16 +++-- .../org/zowe/zdevops/logic/DeleteOperation.kt | 41 +++++++---- .../logic/PerformTsoCommandOperation.kt | 9 +-- .../steps/AllocateDatasetStep/config.jelly | 3 + .../steps/DeleteDatasetStep/config.jelly | 3 + .../DeleteDatasetsByMaskStep/config.jelly | 3 + .../zdevops/config/ZOSConnection/config.jelly | 2 +- .../classic/steps/AllocateDatasetStepSpec.kt | 68 +++++++++++++++++++ .../classic/steps/DeleteDatasetStepSpec.kt | 46 ++++++++++++- .../steps/DeleteDatasetsByMaskStepSpec.kt | 2 +- .../steps/PerformTsoCommandStepSpec.kt | 2 +- 18 files changed, 199 insertions(+), 37 deletions(-) diff --git a/src/main/kotlin/org/zowe/zdevops/classic/steps/AllocateDatasetStep.kt b/src/main/kotlin/org/zowe/zdevops/classic/steps/AllocateDatasetStep.kt index e12f511..52639e9 100644 --- a/src/main/kotlin/org/zowe/zdevops/classic/steps/AllocateDatasetStep.kt +++ b/src/main/kotlin/org/zowe/zdevops/classic/steps/AllocateDatasetStep.kt @@ -55,6 +55,7 @@ constructor( val primary: Int = 1, var secondary: Int, var recFm: RecordFormat, + var failOnExist: Boolean = false, ) : AbstractBuildStep(connectionName){ private var volser: String? = null @@ -177,7 +178,8 @@ constructor( dataClass, avgBlk, dsnType, - dsModel + dsModel, + failOnExist, ) } diff --git a/src/main/kotlin/org/zowe/zdevops/classic/steps/DeleteDatasetStep.kt b/src/main/kotlin/org/zowe/zdevops/classic/steps/DeleteDatasetStep.kt index b694865..6974e54 100644 --- a/src/main/kotlin/org/zowe/zdevops/classic/steps/DeleteDatasetStep.kt +++ b/src/main/kotlin/org/zowe/zdevops/classic/steps/DeleteDatasetStep.kt @@ -22,7 +22,6 @@ import org.zowe.zdevops.Messages import org.zowe.zdevops.classic.AbstractBuildStep import org.zowe.zdevops.logic.deleteDatasetOrMember import org.zowe.zdevops.utils.validateDatasetName -import org.zowe.zdevops.utils.validateFieldIsNotEmpty import org.zowe.zdevops.utils.validateMemberName class DeleteDatasetStep @@ -38,6 +37,7 @@ constructor( connectionName: String, val dsn: String, val member: String?, + val failOnNotExist: Boolean = false , ) : AbstractBuildStep(connectionName) { override fun perform( @@ -46,7 +46,7 @@ constructor( listener: BuildListener, zosConnection: ZOSConnection ) { - deleteDatasetOrMember(dsn, member, zosConnection, listener) + deleteDatasetOrMember(dsn, member, zosConnection, listener, failOnNotExist) } @Extension @@ -67,10 +67,14 @@ constructor( * Checks if the member name is valid * * @param member The dataset member name - * @return FormValidation.ok() if the member name is valid, or an error message otherwise + * @return FormValidation.ok() if either the member name is valid or is not provided, or an error message otherwise */ fun doCheckMember(@QueryParameter member: String): FormValidation? { - return validateMemberName(member)?: validateFieldIsNotEmpty(member) + return if (member.isNotBlank()) { + validateMemberName(member) + } else { + FormValidation.ok() + } } } } \ No newline at end of file diff --git a/src/main/kotlin/org/zowe/zdevops/classic/steps/DeleteDatasetsByMaskStep.kt b/src/main/kotlin/org/zowe/zdevops/classic/steps/DeleteDatasetsByMaskStep.kt index adf3117..ec82d84 100644 --- a/src/main/kotlin/org/zowe/zdevops/classic/steps/DeleteDatasetsByMaskStep.kt +++ b/src/main/kotlin/org/zowe/zdevops/classic/steps/DeleteDatasetsByMaskStep.kt @@ -31,6 +31,7 @@ class DeleteDatasetsByMaskStep constructor( connectionName: String, val dsnMask: String, + val failOnNotExist: Boolean = false, ) : AbstractBuildStep(connectionName) { override fun perform( @@ -39,7 +40,7 @@ constructor( listener: BuildListener, zosConnection: ZOSConnection ) { - deleteDatasetsByMask(dsnMask, zosConnection, listener) + deleteDatasetsByMask(dsnMask, zosConnection, listener, failOnNotExist) } @Extension diff --git a/src/main/kotlin/org/zowe/zdevops/classic/steps/SubmitJobStep.kt b/src/main/kotlin/org/zowe/zdevops/classic/steps/SubmitJobStep.kt index 65dfd04..6a5e548 100644 --- a/src/main/kotlin/org/zowe/zdevops/classic/steps/SubmitJobStep.kt +++ b/src/main/kotlin/org/zowe/zdevops/classic/steps/SubmitJobStep.kt @@ -41,8 +41,8 @@ constructor( val linkBuilder: (String?, String, String) -> String = { jobUrl, jobName, jobId -> "${jobUrl}ws/${jobName}.${jobId}/*view*/" } - val returnCode = submitJobSync(jobName, zosConnection, listener, workspace, build.getEnvironment(listener)["JOB_URL"], linkBuilder) - if (checkRC && !returnCode.equals("CC 0000")) { + val jobResult = submitJobSync(jobName, zosConnection, listener, workspace, build.getEnvironment(listener)["JOB_URL"], linkBuilder) + if (checkRC && !jobResult.equals("CC 0000")) { throw AbortException("Job RC code is not 0000") } } else { diff --git a/src/main/kotlin/org/zowe/zdevops/declarative/jobs/AllocateDatasetDeclarative.kt b/src/main/kotlin/org/zowe/zdevops/declarative/jobs/AllocateDatasetDeclarative.kt index 27e886c..09c63ed 100644 --- a/src/main/kotlin/org/zowe/zdevops/declarative/jobs/AllocateDatasetDeclarative.kt +++ b/src/main/kotlin/org/zowe/zdevops/declarative/jobs/AllocateDatasetDeclarative.kt @@ -46,7 +46,8 @@ constructor( private val dsOrg: DatasetOrganization, private val primary: Int, private var secondary: Int, - private var recFm: RecordFormat) : + private var recFm: RecordFormat, + private var failOnExist: Boolean = false) : AbstractZosmfAction() { private var volser: String? = null @@ -126,7 +127,8 @@ constructor( dataClass, avgBlk, dsnType, - dsModel + dsModel, + failOnExist, ) } diff --git a/src/main/kotlin/org/zowe/zdevops/declarative/jobs/DeleteDatasetDeclarative.kt b/src/main/kotlin/org/zowe/zdevops/declarative/jobs/DeleteDatasetDeclarative.kt index 5b71e1a..fc72a4e 100644 --- a/src/main/kotlin/org/zowe/zdevops/declarative/jobs/DeleteDatasetDeclarative.kt +++ b/src/main/kotlin/org/zowe/zdevops/declarative/jobs/DeleteDatasetDeclarative.kt @@ -66,6 +66,7 @@ class DeleteDatasetDeclarative @DataBoundConstructor constructor( private var dsn: String = "" private var member: String = "" + private var failOnNotExist: Boolean = false @DataBoundSetter fun setDsn(dsn: String) { this.dsn = dsn } @@ -73,6 +74,9 @@ class DeleteDatasetDeclarative @DataBoundConstructor constructor( @DataBoundSetter fun setMember(member: String) { this.member = member } + @DataBoundSetter + fun setFailOnNotExist(failOnNotExist: Boolean) { this.failOnNotExist = failOnNotExist } + override val exceptionMessage: String = zMessages.zdevops_deleting_ds_fail() override fun perform( @@ -83,7 +87,7 @@ class DeleteDatasetDeclarative @DataBoundConstructor constructor( listener: TaskListener, zosConnection: ZOSConnection ) { - deleteDatasetOrMember(dsn, member, zosConnection, listener) + deleteDatasetOrMember(dsn, member, zosConnection, listener, failOnNotExist) } @Symbol("deleteDataset") diff --git a/src/main/kotlin/org/zowe/zdevops/declarative/jobs/DeleteDatasetsByMaskDeclarative.kt b/src/main/kotlin/org/zowe/zdevops/declarative/jobs/DeleteDatasetsByMaskDeclarative.kt index 026d1ae..e91566e 100644 --- a/src/main/kotlin/org/zowe/zdevops/declarative/jobs/DeleteDatasetsByMaskDeclarative.kt +++ b/src/main/kotlin/org/zowe/zdevops/declarative/jobs/DeleteDatasetsByMaskDeclarative.kt @@ -56,10 +56,14 @@ class DeleteDatasetsByMaskDeclarative @DataBoundConstructor constructor( ) : AbstractZosmfAction() { private var mask: String = "" + private var failOnNotExist: Boolean = false @DataBoundSetter fun setMask(mask: String) { this.mask = mask } + @DataBoundSetter + fun setFailOnNotExist(failOnNotExist: Boolean) { this.failOnNotExist = failOnNotExist } + override val exceptionMessage: String = zMessages.zdevops_deleting_ds_fail() override fun perform( @@ -70,7 +74,7 @@ class DeleteDatasetsByMaskDeclarative @DataBoundConstructor constructor( listener: TaskListener, zosConnection: ZOSConnection ) { - deleteDatasetsByMask(mask, zosConnection,listener) + deleteDatasetsByMask(mask, zosConnection,listener, failOnNotExist) } diff --git a/src/main/kotlin/org/zowe/zdevops/logic/AllocateOperation.kt b/src/main/kotlin/org/zowe/zdevops/logic/AllocateOperation.kt index ad6dc58..ca24225 100644 --- a/src/main/kotlin/org/zowe/zdevops/logic/AllocateOperation.kt +++ b/src/main/kotlin/org/zowe/zdevops/logic/AllocateOperation.kt @@ -57,8 +57,8 @@ fun allocateDataset(listener: TaskListener, dataClass: String?, avgBlk: Int?, dsnType: DsnameType?, - dsModel: String? - + dsModel: String?, + failOnExist: Boolean, ) { listener.logger.println(Messages.zdevops_declarative_DSN_allocating(dsn, zosConnection.host, zosConnection.zosmfPort)) val alcParms = CreateDataset( @@ -79,6 +79,14 @@ fun allocateDataset(listener: TaskListener, dsnType, dsModel ) - ZosDsn(zosConnection).createDsn(dsn, alcParms) - listener.logger.println(Messages.zdevops_declarative_DSN_allocated_success(dsn)) + try { + ZosDsn(zosConnection).createDsn(dsn, alcParms) + listener.logger.println(Messages.zdevops_declarative_DSN_allocated_success(dsn)) + } catch (allocateDsEx: Exception) { + listener.logger.println("Dataset allocation failed. Reason: $allocateDsEx") + if(failOnExist) { + throw allocateDsEx + } + listener.logger.println("The `failOnExist` option is set to false. Continuing with execution.") + } } diff --git a/src/main/kotlin/org/zowe/zdevops/logic/DeleteOperation.kt b/src/main/kotlin/org/zowe/zdevops/logic/DeleteOperation.kt index 4f139c2..6048c5f 100644 --- a/src/main/kotlin/org/zowe/zdevops/logic/DeleteOperation.kt +++ b/src/main/kotlin/org/zowe/zdevops/logic/DeleteOperation.kt @@ -29,22 +29,31 @@ private val successMessage: String = zMessages.zdevops_deleting_ds_success() * @param listener The task listener to log information and handle exceptions. * @throws AbortException If the mask is empty or no matching datasets are found. */ -fun deleteDatasetsByMask(mask: String, zosConnection: ZOSConnection, listener: TaskListener) { +fun deleteDatasetsByMask(mask: String, zosConnection: ZOSConnection, listener: TaskListener, failOnNotExist: Boolean) { if (mask.isEmpty()) { throw AbortException(zMessages.zdevops_deleting_datasets_by_mask_but_mask_is_empty()) } listener.logger.println(zMessages.zdevops_deleting_ds_by_mask(mask)) - val dsnList = ZosDsnList(zosConnection).listDsn(mask, ListParams()) - if (dsnList.items.isEmpty()) { - throw AbortException(zMessages.zdevops_deleting_ds_fail_no_matching_mask()) - } - dsnList.items.forEach { - runMFTryCatchWrappedQuery(listener) { - listener.logger.println(zMessages.zdevops_deleting_ds(it.name, zosConnection.host, zosConnection.zosmfPort)) - ZosDsn(zosConnection).deleteDsn(it.name) + try { + val dsnList = ZosDsnList(zosConnection).listDsn(mask, ListParams()) + if (dsnList.items.isEmpty()) { + throw AbortException(zMessages.zdevops_deleting_ds_fail_no_matching_mask()) + } + dsnList.items.forEach { + runMFTryCatchWrappedQuery(listener) { + listener.logger.println(zMessages.zdevops_deleting_ds(it.name, zosConnection.host, zosConnection.zosmfPort)) + ZosDsn(zosConnection).deleteDsn(it.name) + } } + listener.logger.println(successMessage) + } catch (doesNotExistEx: Exception) { + if(failOnNotExist) { + throw doesNotExistEx + } + listener.logger.println("Reason: $doesNotExistEx") + // TODO I wanna have the dataset name here - it's inside exception message? + listener.logger.println("Dataset deletion failed, but the `failOnNotExist` option is set to false. Continuing with execution.") } - listener.logger.println(successMessage) } /** @@ -56,22 +65,28 @@ fun deleteDatasetsByMask(mask: String, zosConnection: ZOSConnection, listener: T * @param listener The task listener to log information and handle exceptions. * @throws AbortException If the dataset name is empty or the member name is invalid. */ -fun deleteDatasetOrMember(dsn: String, member: String?, zosConnection: ZOSConnection, listener: TaskListener) { +fun deleteDatasetOrMember(dsn: String, member: String?, zosConnection: ZOSConnection, listener: TaskListener, failOnNotExist: Boolean) { if (dsn.isEmpty()) { throw AbortException(zMessages.zdevops_deleting_ds_fail_dsn_param_empty()) } val logMessage = if (!member.isNullOrEmpty()) zMessages.zdevops_deleting_ds_member(member, dsn, zosConnection.host, zosConnection.zosmfPort) else zMessages.zdevops_deleting_ds(dsn, zosConnection.host, zosConnection.zosmfPort) listener.logger.println(logMessage) - runMFTryCatchWrappedQuery(listener) { + try { if (!member.isNullOrEmpty()) { isMemberNameValid(member) ZosDsn(zosConnection).deleteDsn(dsn, member) } else { ZosDsn(zosConnection).deleteDsn(dsn) } + listener.logger.println(successMessage) + } catch (doesNotExistEx: Exception) { + if(failOnNotExist) { + throw doesNotExistEx + } + listener.logger.println("Reason: $doesNotExistEx") + listener.logger.println("Dataset deletion failed, but the `failOnNotExist` option is set to false. Continuing with execution.") } - listener.logger.println(successMessage) } /** diff --git a/src/main/kotlin/org/zowe/zdevops/logic/PerformTsoCommandOperation.kt b/src/main/kotlin/org/zowe/zdevops/logic/PerformTsoCommandOperation.kt index dabb4c1..c6aeb07 100644 --- a/src/main/kotlin/org/zowe/zdevops/logic/PerformTsoCommandOperation.kt +++ b/src/main/kotlin/org/zowe/zdevops/logic/PerformTsoCommandOperation.kt @@ -14,6 +14,7 @@ import hudson.AbortException import hudson.model.TaskListener import org.zowe.kotlinsdk.zowe.client.sdk.core.ZOSConnection import org.zowe.kotlinsdk.zowe.client.sdk.zostso.IssueTso +import org.zowe.kotlinsdk.zowe.client.sdk.zostso.input.StartTsoParams import org.zowe.zdevops.Messages /** @@ -36,12 +37,12 @@ fun performTsoCommand( command: String, ) { listener.logger.println(Messages.zdevops_issue_TSO_command(command)) - runCatching { - val tsoCommandResponse = IssueTso(zosConnection).issueTsoCommand(acct, command) + try { + val tsoCommandResponse = IssueTso(zosConnection).issueTsoCommand(acct, command, StartTsoParams(), failOnPrompt = true) listener.logger.println(tsoCommandResponse.commandResponses) - }.onFailure { + } catch (ex: Exception) { listener.logger.println(Messages.zdevops_TSO_command_fail()) - throw AbortException(Messages.zdevops_TSO_command_fail()) + throw ex } listener.logger.println(Messages.zdevops_TSO_command_success()) } \ No newline at end of file diff --git a/src/main/resources/org/zowe/zdevops/classic/steps/AllocateDatasetStep/config.jelly b/src/main/resources/org/zowe/zdevops/classic/steps/AllocateDatasetStep/config.jelly index f17a368..adca495 100644 --- a/src/main/resources/org/zowe/zdevops/classic/steps/AllocateDatasetStep/config.jelly +++ b/src/main/resources/org/zowe/zdevops/classic/steps/AllocateDatasetStep/config.jelly @@ -86,4 +86,7 @@ + + + \ No newline at end of file diff --git a/src/main/resources/org/zowe/zdevops/classic/steps/DeleteDatasetStep/config.jelly b/src/main/resources/org/zowe/zdevops/classic/steps/DeleteDatasetStep/config.jelly index bea0c67..aa711c2 100644 --- a/src/main/resources/org/zowe/zdevops/classic/steps/DeleteDatasetStep/config.jelly +++ b/src/main/resources/org/zowe/zdevops/classic/steps/DeleteDatasetStep/config.jelly @@ -12,4 +12,7 @@ + + + \ No newline at end of file diff --git a/src/main/resources/org/zowe/zdevops/classic/steps/DeleteDatasetsByMaskStep/config.jelly b/src/main/resources/org/zowe/zdevops/classic/steps/DeleteDatasetsByMaskStep/config.jelly index f0ccd34..6fabee9 100644 --- a/src/main/resources/org/zowe/zdevops/classic/steps/DeleteDatasetsByMaskStep/config.jelly +++ b/src/main/resources/org/zowe/zdevops/classic/steps/DeleteDatasetsByMaskStep/config.jelly @@ -9,4 +9,7 @@ + + + \ No newline at end of file diff --git a/src/main/resources/org/zowe/zdevops/config/ZOSConnection/config.jelly b/src/main/resources/org/zowe/zdevops/config/ZOSConnection/config.jelly index 414507a..8280a21 100644 --- a/src/main/resources/org/zowe/zdevops/config/ZOSConnection/config.jelly +++ b/src/main/resources/org/zowe/zdevops/config/ZOSConnection/config.jelly @@ -7,7 +7,7 @@ - + diff --git a/src/test/kotlin/org/zowe/zdevops/classic/steps/AllocateDatasetStepSpec.kt b/src/test/kotlin/org/zowe/zdevops/classic/steps/AllocateDatasetStepSpec.kt index a071ca6..1b2324f 100644 --- a/src/test/kotlin/org/zowe/zdevops/classic/steps/AllocateDatasetStepSpec.kt +++ b/src/test/kotlin/org/zowe/zdevops/classic/steps/AllocateDatasetStepSpec.kt @@ -124,6 +124,74 @@ class AllocateDatasetStepSpec : ShouldSpec({ assertSoftly { isDatasetAllocating shouldBe true } assertSoftly { isDatasetAllocated shouldBe true } } + + should("fail as such dataset already exists and failOnExist is set to true") { + var isDatasetAllocating = false + var isFailedToAllocate = false + val taskListener = object : TestBuildListener() { + override fun getLogger(): PrintStream { + val logger = mockk() + every { + logger.println(any()) + } answers { + if (firstArg().contains("Allocating dataset")) { + isDatasetAllocating = true + } else if (firstArg().contains("Dataset allocation failed.")) { + isFailedToAllocate = true + } else { + fail("Unexpected logger message: ${firstArg()}") + } + } + return logger + } + } + val launcher = TestLauncher(taskListener, virtualChannel) + + responseDispatcher.injectEndpoint( + this.testCase.name.testName, + { it?.requestLine?.contains("/zosmf/restfiles/ds/") ?: false }, + { MockResponse().setResponseCode(409) } + ) + + val allocateDatasetStepInst = spyk( + AllocateDatasetStep( + "test", + "TEST.IJMP.DATASET1", + DatasetOrganization.PS, + 1, + 0, + RecordFormat.F, + failOnExist = true + ) + ) + allocateDatasetStepInst.setAlcUnit(AllocationUnit.CYL) + allocateDatasetStepInst.setStorClass("") + allocateDatasetStepInst.setStorClass("TEST") + allocateDatasetStepInst.setMgntClass("") + allocateDatasetStepInst.setMgntClass("TEST") + allocateDatasetStepInst.setDataClass("") + allocateDatasetStepInst.setDataClass("TEST") + allocateDatasetStepInst.setVolser("") + allocateDatasetStepInst.setVolser("TEST") + allocateDatasetStepInst.setUnit("") + allocateDatasetStepInst.setUnit("TEST") + allocateDatasetStepInst.setLrecl(3120) + allocateDatasetStepInst.setBlkSize(3120) + allocateDatasetStepInst.setDsnType(DsnameType.BASIC) + try { + allocateDatasetStepInst.perform( + build, + launcher, + taskListener, + zosConnection + ) + } catch (ex: Exception) { + isFailedToAllocate = true + } + assertSoftly { isDatasetAllocating shouldBe true } + assertSoftly { isFailedToAllocate shouldBe true } + } + } val descriptor = AllocateDatasetStep.DescriptorImpl() diff --git a/src/test/kotlin/org/zowe/zdevops/classic/steps/DeleteDatasetStepSpec.kt b/src/test/kotlin/org/zowe/zdevops/classic/steps/DeleteDatasetStepSpec.kt index c3d3ab5..f110b9d 100644 --- a/src/test/kotlin/org/zowe/zdevops/classic/steps/DeleteDatasetStepSpec.kt +++ b/src/test/kotlin/org/zowe/zdevops/classic/steps/DeleteDatasetStepSpec.kt @@ -99,8 +99,52 @@ class DeleteDatasetStepSpec : ShouldSpec({ assertSoftly { isDeletingDataset shouldBe true } assertSoftly { isSuccessfullyDeleted shouldBe true } } + + should("succeed as there is no such dataset, but failOnNotExist is set to false") { + var isDeletingDataset = false + var isContinuingExecution = false + val taskListener = object : TestBuildListener() { + override fun getLogger(): PrintStream { + val logger = mockk() + every { + logger.println(any()) + } answers { + if (firstArg().contains("Deleting dataset")) { + isDeletingDataset = true + } else if (firstArg().contains("Dataset deletion failed, but the `failOnNotExist` option is set to false.") + || firstArg().contains("Reason")) { + isContinuingExecution = true + } else { + fail("Unexpected logger message: ${firstArg()}") + } + } + return logger + } + } + val launcher = TestLauncher(taskListener, virtualChannel) + + responseDispatcher.injectEndpoint( + this.testCase.name.testName, + { it?.requestLine?.contains(Regex("DELETE /zosmf/restfiles/ds/.* HTTP/.*")) ?: false }, + { MockResponse().setBody("{}").setResponseCode(404) } + ) + + val deleteDatasetStep = spyk( + DeleteDatasetStep("test", "TEST.IJMP.DATASET1", member = null, failOnNotExist = false) + ) + deleteDatasetStep.perform( + build, + launcher, + taskListener, + zosConnection + ) + + assertSoftly { isDeletingDataset shouldBe true } + assertSoftly { isContinuingExecution shouldBe true } + } } + val descriptor = DeleteDatasetStep.DescriptorImpl() context("classic/steps module: DeleteDatasetStep.DescriptorImpl") { @@ -110,7 +154,7 @@ class DeleteDatasetStepSpec : ShouldSpec({ } should("validate member name") { - descriptor.doCheckMember("") shouldBe FormValidation.error(Messages.zdevops_value_up_to_eight_in_length_validation()) + descriptor.doCheckMember("") shouldBe FormValidation.ok() descriptor.doCheckMember("@MY_DS") shouldBe FormValidation.warning(Messages.zdevops_member_name_is_invalid_validation()) descriptor.doCheckMember("DSNAME") shouldBe FormValidation.ok() } diff --git a/src/test/kotlin/org/zowe/zdevops/classic/steps/DeleteDatasetsByMaskStepSpec.kt b/src/test/kotlin/org/zowe/zdevops/classic/steps/DeleteDatasetsByMaskStepSpec.kt index 1b1aeb7..87f0ea8 100644 --- a/src/test/kotlin/org/zowe/zdevops/classic/steps/DeleteDatasetsByMaskStepSpec.kt +++ b/src/test/kotlin/org/zowe/zdevops/classic/steps/DeleteDatasetsByMaskStepSpec.kt @@ -132,7 +132,7 @@ class DeleteDatasetsByMaskStepSpec : ShouldSpec({ ) val deleteDatasetDecl = spyk( - DeleteDatasetsByMaskStep("test", "TEST.IJMP.DATASET%.NONE") + DeleteDatasetsByMaskStep("test", "TEST.IJMP.DATASET%.NONE", failOnNotExist = true) ) shouldThrow { deleteDatasetDecl.perform( diff --git a/src/test/kotlin/org/zowe/zdevops/classic/steps/PerformTsoCommandStepSpec.kt b/src/test/kotlin/org/zowe/zdevops/classic/steps/PerformTsoCommandStepSpec.kt index ea837f8..ff95f15 100644 --- a/src/test/kotlin/org/zowe/zdevops/classic/steps/PerformTsoCommandStepSpec.kt +++ b/src/test/kotlin/org/zowe/zdevops/classic/steps/PerformTsoCommandStepSpec.kt @@ -120,7 +120,7 @@ class PerformTsoCommandStepSpec : ShouldSpec({ assertSoftly { isPreExecuteStage shouldBe true } assertSoftly { isCommandExecuted shouldBe true } } - should("fail SubmitJobStep operation") { + should("fail PerformTsoCommand operation") { var isPreExecuteStage = false var isExecuteCommandFailLogged = false val taskListener = object : TestBuildListener() { From 880cf4b8235751a0c36e0d956b2aab697d95c3dd Mon Sep 17 00:00:00 2001 From: Anatoli Kalbasin <37359814+callbacksin@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:39:43 +0300 Subject: [PATCH 3/4] IJMP-1668- Fix connection logs (#78) * handle connection logs Signed-off-by: Anatoli Kalbasin --- README.md | 2 +- pom.xml | 36 ++++++++++------ .../zowe/zdevops/classic/AbstractBuildStep.kt | 3 ++ .../org/zowe/zdevops/config/ZOSConnection.kt | 11 +++-- .../declarative/AbstractZosmfAction.kt | 4 ++ .../zdevops/utils/ConnectionValidation.kt | 42 +++++++++++++++++++ 6 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 src/main/kotlin/org/zowe/zdevops/utils/ConnectionValidation.kt diff --git a/README.md b/README.md index 16aa3b4..fa8a0c6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Zowe zDevOps Jenkins plugin -## About the plugin +## About the plugin The Zowe zDevOps Jenkins Plugin by [IBA Group](https://ibagroupit.com/?utm_campaign=IBA_W-Mainframe&utm_source=jenkins&utm_medium=referral&utm_content=description_zdevops) is an open-source, secure , and reliable agent-less Jenkins plugin that makes it possible to perform most of the actual tasks on the mainframe, managing it with a modern native mainframe zOSMF REST API and the capabilities of available zOSMF SDKs. ## Advantages diff --git a/pom.xml b/pom.xml index f507f17..2a9f4d9 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.jenkins-ci.plugins plugin - 4.78 + 4.78 @@ -19,7 +19,7 @@ - 0.2.0- + 0.2.0 -SNAPSHOT 2.414.3 17 @@ -375,22 +375,12 @@ 2.10.1 - - org.jenkins-ci.plugins - script-security - - org.apache.commons commons-lang3 3.12.0 - - org.jenkins-ci - annotation-indexer - - com.squareup.okhttp3 okhttp @@ -439,6 +429,28 @@ test + + + + org.jenkins-ci + annotation-indexer + + + + org.jenkins-ci.plugins + script-security + + + + org.jenkins-ci.plugins + credentials + + + + org.jenkins-ci.plugins.workflow + workflow-step-api + + diff --git a/src/main/kotlin/org/zowe/zdevops/classic/AbstractBuildStep.kt b/src/main/kotlin/org/zowe/zdevops/classic/AbstractBuildStep.kt index c64cca0..6aecdba 100644 --- a/src/main/kotlin/org/zowe/zdevops/classic/AbstractBuildStep.kt +++ b/src/main/kotlin/org/zowe/zdevops/classic/AbstractBuildStep.kt @@ -24,6 +24,7 @@ import org.kohsuke.stapler.QueryParameter import org.zowe.kotlinsdk.zowe.client.sdk.core.ZOSConnection import org.zowe.zdevops.Messages import org.zowe.zdevops.config.ZOSConnectionList +import org.zowe.zdevops.utils.validateConnection import java.io.IOException import java.io.PrintWriter import java.io.StringWriter @@ -71,6 +72,8 @@ abstract class AbstractBuildStep(val connectionName: String) : Builder(), Simple connURL.host, connURL.port.toString(), connection.username, connection.password, connURL.protocol ) + validateConnection(zosConnection) + perform(build, launcher, listener, zosConnection) return true } diff --git a/src/main/kotlin/org/zowe/zdevops/config/ZOSConnection.kt b/src/main/kotlin/org/zowe/zdevops/config/ZOSConnection.kt index 75b68aa..e81e35d 100644 --- a/src/main/kotlin/org/zowe/zdevops/config/ZOSConnection.kt +++ b/src/main/kotlin/org/zowe/zdevops/config/ZOSConnection.kt @@ -16,10 +16,6 @@ import com.cloudbees.plugins.credentials.common.StandardCredentials import com.cloudbees.plugins.credentials.common.StandardListBoxModel import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder -import org.zowe.kotlinsdk.zowe.client.sdk.zosfiles.ZosDsnList -import org.zowe.kotlinsdk.zowe.client.sdk.zosfiles.input.ListParams -import org.zowe.zdevops.Messages -import org.zowe.zdevops.declarative.jobs.zMessages import hudson.Extension import hudson.model.AbstractDescribableImpl import hudson.model.Descriptor @@ -32,6 +28,9 @@ import net.sf.json.JSONObject import org.kohsuke.stapler.DataBoundConstructor import org.kohsuke.stapler.QueryParameter import org.kohsuke.stapler.StaplerRequest +import org.zowe.zdevops.Messages +import org.zowe.zdevops.declarative.jobs.zMessages +import org.zowe.zdevops.utils.getTestDatasetList import java.io.IOException import java.io.ObjectInputStream import java.io.ObjectOutputStream @@ -119,9 +118,9 @@ constructor( val testConnection = org.zowe.kotlinsdk.zowe.client.sdk.core.ZOSConnection( connURL.host, connURL.port.toString(), credentials.username, credentials.password.plainText, connURL.protocol ) - ZosDsnList(testConnection).listDsn(zMessages.zdevops_config_ZOSConnection_validation_testDS(), ListParams()) + getTestDatasetList(testConnection) }.onFailure { - return FormValidation.error(zMessages.zdevops_config_ZOSConnection_validation_error()); + return FormValidation.error("${zMessages.zdevops_config_ZOSConnection_validation_error()}\n(${it.message})"); } return FormValidation.ok(zMessages.zdevops_config_ZOSConnection_validation_success()) } diff --git a/src/main/kotlin/org/zowe/zdevops/declarative/AbstractZosmfAction.kt b/src/main/kotlin/org/zowe/zdevops/declarative/AbstractZosmfAction.kt index 5cf4b4e..7537d69 100644 --- a/src/main/kotlin/org/zowe/zdevops/declarative/AbstractZosmfAction.kt +++ b/src/main/kotlin/org/zowe/zdevops/declarative/AbstractZosmfAction.kt @@ -23,6 +23,7 @@ import jenkins.tasks.SimpleBuildStep import org.zowe.kotlinsdk.zowe.client.sdk.core.ZOSConnection import org.zowe.zdevops.Messages import org.zowe.zdevops.config.ZOSConnectionList +import org.zowe.zdevops.utils.validateConnection import java.io.PrintWriter import java.io.StringWriter import java.net.URL @@ -50,6 +51,9 @@ abstract class AbstractZosmfAction : Builder(), SimpleBuildStep { val zoweConnection = ZOSConnection( connURL.host, connURL.port.toString(), connection.username, connection.password, connURL.protocol ) + + validateConnection(zoweConnection) + runCatching { perform(run, workspace, env, launcher, listener, zoweConnection) }.onFailure { diff --git a/src/main/kotlin/org/zowe/zdevops/utils/ConnectionValidation.kt b/src/main/kotlin/org/zowe/zdevops/utils/ConnectionValidation.kt new file mode 100644 index 0000000..d5211a0 --- /dev/null +++ b/src/main/kotlin/org/zowe/zdevops/utils/ConnectionValidation.kt @@ -0,0 +1,42 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright IBA Group 2024 + */ + +package org.zowe.zdevops.utils + +import hudson.AbortException +import org.zowe.kotlinsdk.zowe.client.sdk.core.ZOSConnection +import org.zowe.kotlinsdk.zowe.client.sdk.zosfiles.ZosDsnList +import org.zowe.kotlinsdk.zowe.client.sdk.zosfiles.input.ListParams +import org.zowe.zdevops.Messages + +/** + * Gets a list of datasets + * Calls the listDsn function of ZosDsnList to list data set names. + * Passes a test data set name ('HELLO.THERE'). + * + * @param zosConnection The ZOSConnection object representing the connection to the z/OS system. + */ +fun getTestDatasetList(zosConnection: ZOSConnection) { + ZosDsnList(zosConnection).listDsn(Messages.zdevops_config_ZOSConnection_validation_testDS(), ListParams()) +} + +/** + * Validates a z/OS connection. + * + * @param zosConnection The ZOSConnection object representing the connection to the z/OS system. + */ +fun validateConnection(zosConnection: ZOSConnection) { + try { + getTestDatasetList(zosConnection) + } catch (connectException: Exception) { + val connExMessage = "Failed to connect to z/OS (${zosConnection.user}@${zosConnection.host}:${zosConnection.zosmfPort}): ${connectException.message}" + throw AbortException(connExMessage) + } +} \ No newline at end of file From fd4f4070ed8c01602eae2317a01fa5e32dd14c39 Mon Sep 17 00:00:00 2001 From: Anatoli Kalbasin <37359814+callbacksin@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:05:51 +0300 Subject: [PATCH 4/4] IJMP-1710 - Validate connection once during zosmf declarative step execution (#83) * implement connection validation Signed-off-by: Anatoli Kalbasin --- README.md | 299 +++++++++++++++--- .../org/zowe/zdevops/config/ZOSConnection.kt | 2 +- .../declarative/AbstractZosmfAction.kt | 20 +- .../declarative/ZosmfStepDeclarative.kt | 9 +- .../zdevops/utils/ConnectionValidation.kt | 34 +- 5 files changed, 296 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index fa8a0c6..8c003ab 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,13 @@ ## About the plugin The Zowe zDevOps Jenkins Plugin by [IBA Group](https://ibagroupit.com/?utm_campaign=IBA_W-Mainframe&utm_source=jenkins&utm_medium=referral&utm_content=description_zdevops) is an open-source, secure , and reliable agent-less Jenkins plugin that makes it possible to perform most of the actual tasks on the mainframe, managing it with a modern native mainframe zOSMF REST API and the capabilities of available zOSMF SDKs. -## Advantages +## Main features - Secure and modern connection of Jenkins to the mainframes through the use of zOSMF REST API. - The functionality is based on the Kotlin SDK methods, such as JCL jobs submission, download, allocate, write to the dataset, etc., with a log collected upon completion. -- Multiple connections to various mainframes—z/OS Connections List where you can save all the necessary systems and credentials; all data is safely stored under the protection of Jenkins Credentials manager. -- Agent-less. +- Multiple connections to various mainframes — z/OS Connections List where you can save all the necessary systems and credentials (all data is safely stored under the protection of Jenkins Credentials manager). +- Agent-less solution. - z/OSMF connection validation. +- Convenient user interface panels for working with the mainframe - Fast execution and functional extensibility. ## About us @@ -24,6 +25,254 @@ Please feel free to contact us or schedule a call with our Mainframe DevOps expe Thank you for considering IBA Group for your mainframe needs. +## Before use - Plugin configuration +After successfully installing the plugin, you need to configure it for further work - this will require a minimum of actions. +1. Move to “Manage Jenkins” -> “Configure System / System” -> scroll to the very bottom of the list of installed plugins and find the panel with the name - “z/OS Connection List” +2. This setting allows you to add all necessary z/OS systems and configure access to them. + It is necessary to set the connection name (it is also the ID for declarative methods in the code). For the example: ```z/os-connection-name``` +3. The URL address and port of the required mainframe to connect via z/OSMF. Example: ```https://:``` +4. Add credentials (Mainframe User ID + Password) under which you can connect to the system. + +You can save as many connections as you like, the system will keep the corresponding user IDs/passwords. + +## Declarative methods brief list +```groovy +stage ("stage-name") { + steps { + // ... + zosmf("z/os-connection-name") { + submitJob "//'EXAMPLE.DATASET(MEMBER)'" + submitJobSync "//'EXAMPLE.DATASET(MEMBER)'" + downloadDS "EXAMPLE.DATASET(MEMBER)" + downloadDS dsn:"EXAMPLE.DATASET(MEMBER)", vol:"VOL001" + allocateDS dsn:"EXAMPLE.DATASET", alcUnit:"TRK", dsOrg:"PS", primary:1, secondary:1, recFm:"FB" + writeFileToDS dsn:"EXAMPLE.DATASET", file:"workspaceFile" + writeFileToDS dsn:"EXAMPLE.DATASET", file:"D:\\files\\localFile" + writeToDS dsn:"EXAMPLE.DATASET", text:"Write this string to dataset" + writeFileToMember dsn:"EXAMPLE.DATASET", member:"MEMBER", file:"workspaceFile" + writeFileToMember dsn:"EXAMPLE.DATASET", member:"MEMBER", file:"D:\\files\\localFile" + writeToMember dsn:"EXAMPLE.DATASET", member:"MEMBER", text:"Write this string to member" + + writeToFile destFile: "u/USER/myfile", text: "Write this string to file" + writeFileToFile destFile: "u/USER/myfile", sourceFile: "myfile.txt" + writeFileToFile destFile: "u/USER/myfile", sourceFile: "myfile.txt", binary: "true" + + deleteDataset dsn:"EXAMPLE.DATASET" + deleteDataset dsn:"EXAMPLE.DATASET", member:"MEMBER" + deleteDatasetsByMask mask:"EXAMPLE.DATASET.*" + } + // ... + } +} +``` + +## Declarative Methods Detail Description + +### allocateDS - Represents an action for allocating a dataset in a declarative style +```groovy +allocateDS dsn:"EXAMPLE.DATASET", dsOrg:"PS", primary:1, secondary:1, recFm:"FB" +``` +**Mandatory Parameters:** + * ```dsn:"EXAMPLE.DATASET"``` - The name of the dataset to be allocated + * ```dsOrg:"PS"``` - The dataset organization (could be only PO, POE, PS, VS) + * ```primary:"1"``` - The primary allocation size in cylinders or tracks + * ```secondary:"1"``` - The secondary allocation size in cylinders or tracks + * ```recFm:"FB"``` - The record format (could be only F, FB, V, VB, U, VSAM, VA) + +**Optional parms:** + * ```volser:"YOURVOL"``` - Volume serial number where the dataset will be allocated. + * ```unit:"SYSDA"``` - Specifies the type of storage device. SYSDA is a common direct access storage device. + * ```alcUnit:"TRK"``` - Allocation units (CYL for cylinders, TRK for tracks). + * ```dirBlk:"5"``` - Directory block records. + * ```blkSize:"800"``` - BLKSIZE=800: Block size of 800 bytes. + * ```lrecl:"80"``` - Logical record length. + * ```storClass:"STORAGECLASS"``` - Storage class for SMS-managed datasets. + * ```mgntClass:"MGMTCLASS"``` - Management class for SMS-managed datasets. + * ```dataClass:"DATACLASS"``` - Data class for SMS-managed datasets. + * ```avgBlk:"10"``` - Average block length. + * ```dsnType:"LIBRARY"``` - Specifies the type of dataset, LIBRARY for a PDS or PDSE. + * ```dsModel:"MODEL.DATASET.NAME"``` - Data set model is a predefined set of attributes that can be used to allocate new data sets with the same characteristics ("LIKE" parameter). + + +### deleteDataset - Represents an action for deleting datasets and members in a declarative style +```groovy +deleteDataset dsn:"EXAMPLE.DATASET" +``` +**Mandatory Parameters:** + * ```dsn:"EXAMPLE.DATASET"``` - Sequential or library dataset name for deletion + * ```member:"MEMBER"``` - Dataset member name for deletion + +**Expected behavior under various deletion scenarios:** + +* To delete a member from the library, the dsn and member parameters must be specified: + ``` + deleteDataset dsn:"EXAMPLE.DATASET", member:"MEMBER" + ``` + +* You cannot delete a VSAM dataset this way. Otherwise, you will get output similar to: + ``` + Deleting dataset EXAMPLE.VSAM.DATASET with connection :10443 + ISRZ002 Deallocation failed - Deallocation failed for data set 'EXAMPLE.VSAM.DATASET' + ``` + +* What do you get if a dataset does not exist? + + ``` + Deleting dataset EXAMPLE.DS.DOES.NOT.EXIST with connection :10443 + ISRZ002 Data set not cataloged - 'EXAMPLE.DS.DOES.NOT.EXIST' was not found in catalog. + ``` + +* What do you get if a dataset is busy by a user or a program? + + ``` + Deleting dataset EXAMPLE.DS.ISUSED.BY.USER with connection :10443 + ISRZ002 Data set in use - Data set 'EXAMPLE.DS.ISUSED.BY.USER' in use by another user, try later or enter HELP for a list of jobs and users allocated to 'EXAMPLE.DS.ISUSED.BY.USER'. + ``` + +## Use case example +Here you can find an example of a minimal declarative Jenkins pipeline for execution, testing and further modification for your personal needs. +Pipeline can be used either directly inside the ```Pipeline``` code block in the Jenkins server, or in a ```Jenkinsfile``` stored in Git +This pipeline example uses all currently available methods and functionality of the Zowe zDevOps plugin. + +**Steps to Execute the Pipeline:** +1. Add a zosmf connection in settings (“Manage Jenkins” -> “Configure System / System” -> z/OS Connection List). Enter a connection name, zosmf url, username and password. +2. Create a new Jenkins item -> ```Pipeline``` and open its configuration. +3. In the ```Pipeline``` section, paste the code from the example below and replace all the necessary variables with your data +4. Done, enjoy the minimal ready-made pipeline template! + +```groovy +pipeline { + agent any + + environment { + // Define environment variables + GIT_REPOSITORY_URL = 'https://github.com/your-username/your-repo.git' // Replace with your GitHub URL + GIT_BRANCH = 'main' // Replace with your GitHub branch name + GIT_USER_CREDENTIAL_ID = 'jenkins-cred-key' // Replace with your Jenkins GitHub credential ID + ZOS_CONN_ID = 'z/os-connection-name' // Replace with your z/OS Connection ID from zDevOps plugin settings + HLQ = 'HLQ' // Replace with your z/OS high-level qualifier (HLQ) + PS_DATASET_1 = "${HLQ}.NEW.TEST1" // OPTIONAL: Replace with the dataset names you need + PS_DATASET_2 = "${HLQ}.NEW.TEST2" // OPTIONAL + PO_DATASET = "${HLQ}.NEW.TEST3" // OPTIONAL + PO_MEMBER = "NEWMEM" // OPTIONAL + JCL_JOB_TEMPLATE = "jcl_job_example" // Replace with the name of your file that contains the JCL job code + JIRA_URL = 'https://your-jira-instance.atlassian.net' // Replace with your Jira URL + JIRA_USER = 'your-jira-email@example.com' // Replace with your Jira user email + JIRA_API_TOKEN = 'your-jira-api-token' // Replace with your Jira API token + JIRA_ISSUE_KEY = 'PROJECT-123' // Replace with your Jira issue key + } + + stages { + stage('Checkout') { + steps { + // Checkout the source code from Git + checkout scmGit( + branches: [[name: "${GIT_BRANCH}"]], + userRemoteConfigs: [[credentialsId: "${GIT_USER_CREDENTIAL_ID}", + url: "${GIT_REPOSITORY_URL}"]]) + } + } + + stage('Allocate DSs') { + steps { + zosmf("${ZOS_CONN_ID}") { + allocateDS dsn:"${PS_DATASET_1}", dsOrg:"PS", primary:1, secondary:1, recFm:"FB" + allocateDS dsn:"${PS_DATASET_2}", dsOrg:"PS", primary:1, secondary:1, recFm:"FB", alcUnit:"TRK" + allocateDS dsn:"${PO_DATASET}(${PO_MEMBER})", dsOrg:"PO", primary:1, secondary:1, recFm:"FB" + } + } + } + + stage('Add JCL content') { + steps { + script { + // Read the content of the JCL job into a variable + env.JCL_CONTENT = readFile("${JCL_JOB_TEMPLATE}").trim() + // Print the content of the file (for debugging purposes) + echo "JCL job content:\n${env.JCL_CONTENT}" + } + zosmf("${ZOS_CONN_ID}") { + writeFileToDS dsn:"${PS_DATASET_2}", file:"${JCL_JOB_TEMPLATE}" + writeFileToMember dsn:"${PO_DATASET}", member:"${PO_MEMBER}", file:"${JCL_JOB_TEMPLATE}" + writeToDS dsn:"${PS_DATASET_1}", text:"${env.JCL_CONTENT}" + } + } + } + + stage('Add USS content') { + steps { + zosmf("${ZOS_CONN_ID}") { + writeToFile destFile: "u/${HLQ}/test_file1", text: "${env.JCL_CONTENT}" + writeFileToFile destFile: "u/${HLQ}/test_file2", sourceFile: "${JCL_JOB_TEMPLATE}", binary: "true" + } + } + } + + stage('Submit JCL jobs') { + steps { + zosmf("${ZOS_CONN_ID}") { + submitJob "//'${PS_DATASET_1}'" + submitJobSync "//'${PO_DATASET}(NEWMEM)'" + } + } + } + + stage('Download datasets') { + steps { + zosmf("${ZOS_CONN_ID}") { + downloadDS "${PS_DATASET_1}" + downloadDS dsn:"${PS_DATASET_2}" + } + } + } + + stage('Clean up') { + steps { + zosmf("${ZOS_CONN_ID}") { + deleteDataset dsn:"${PS_DATASET_1}" + deleteDatasetsByMask mask:"${HLQ}.NEW.*" + } + } + } + } + + post { + always { + script { + def jiraStatus = currentBuild.currentResult == 'SUCCESS' ? 'Build Successful' : 'Build Failed' + def jiraComment = """ + { + "body": "Jenkins build ${jiraStatus} for Job ${env.JOB_NAME} - Build #${env.BUILD_NUMBER}. + [View the build here|${env.BUILD_URL}]" + } + """ + + httpRequest acceptType: 'APPLICATION_JSON', + contentType: 'APPLICATION_JSON', + httpMode: 'POST', + requestBody: jiraComment, + url: "${JIRA_URL}/rest/api/2/issue/${JIRA_ISSUE_KEY}/comment", + authentication: 'jira-credentials-id' + } + } + + success { + // Notify success (example: send email) + mail to: '${JIRA_USER}', + subject: "SUCCESS: Build ${env.BUILD_NUMBER}", + body: "The build ${env.BUILD_NUMBER} was successful." + } + + failure { + // Notify failure (example: send email) + mail to: '${JIRA_USER}', + subject: "FAILURE: Build ${env.BUILD_NUMBER}", + body: "The build ${env.BUILD_NUMBER} failed. Please check the Jenkins logs for more details." + } + } +} +``` + ## Manual plugin installation by the .hpi executable file The plugin are packaged as self-contained .hpi files, which have all the necessary code, images, and other resources which the plugin needs to operate successfully. @@ -45,50 +294,6 @@ Assuming a .hpi file has been downloaded, a logged-in Jenkins administrat 6. Next you need to login into the Jenkins, move to the “Manage Jenkins” -> “Manage Plugins” -> “Advanced (tab)” -> “Deploy Plugin” (You can select a plugin file from your local system or provide a URL to install a plugin from outside the central plugin repository) -> Specify the path to the generated .hpi/.jpi file (or by dragging the file from Intellij IDEA project to the file upload field in the Jenkins). 7. Click “Deploy”, reboot Jenkins after installation. The Plugin is ready to go! -## Plugin configuration -After successfully installing the plugin, you need to configure it for further work - this will require a minimum of actions. -1. Move to “Manage Jenkins” -> “Configure System” -> scroll down and find the panel with the name - “z/OS Connection List” -2. This setting allows you to add all necessary z/OS systems and configure access to them. - It is necessary to set the connection name (it is also the ID for the call in the code). For the example: ```z/os-connection-name``` -3. The URL address and port of the required mainframe to connect via z/OSMF. Example: ```https://:``` -4. Add credentials (Mainframe User ID + Password) under which you can connect to the system. - -You can save as many connections as you like, the system will keep the corresponding user IDs/passwords. - -## Use case -- Add a zosmf connection in settings (Manage Jenkins -> Configure System -> z/OS Connection List). Enter a connection name, zosmf url, username and password. -- Create a new item -> ```Pipeline``` and open its configuration. - Create a zosmf section inside the steps of the stage and pass the connection name as a parameter of the section. Inside the zosmf body invoke necessary zosmf functions (they will be automatically done in a specified connection context). Take a look at the example below: -```groovy -stage ("stage-name") { - steps { - // ... - zosmf("z/os-connection-name") { - submitJob "//'EXAMPLE.DATASET(JCLJOB)'" - submitJobSync "//'EXAMPLE.DATASET(JCLJOB)'" - downloadDS "USER.LIB(MEMBER)" - downloadDS dsn:"USER.LIB(MEMBER)", vol:"VOL001" - allocateDS dsn:"STV.TEST5", alcUnit:"TRK", dsOrg:"PS", primary:1, secondary:1, recFm:"FB" - writeFileToDS dsn:"USER.DATASET", file:"workspaceFile" - writeFileToDS dsn:"USER.DATASET", file:"D:\\files\\localFile" - writeToDS dsn:"USER.DATASET", text:"Write this string to dataset" - writeFileToMember dsn:"USER.DATASET", member:"MEMBER", file:"workspaceFile" - writeFileToMember dsn:"USER.DATASET", member:"MEMBER", file:"D:\\files\\localFile" - writeToMember dsn:"USER.DATASET", member:"MEMBER", text:"Write this string to member" - - writeToFile destFile: "u/USER/doc", text: "Hello there" - writeFileToFile destFile: "u/USER/doc", sourceFile: "myfile.txt" - writeFileToFile destFile: "u/USER/doc", sourceFile: "myfile.txt", binary: "true" - - deleteDataset dsn:"USER.DATASET" - deleteDataset dsn:"USER.DATASET", member:"MEMBER1" - deleteDatasetsByMask mask:"USER.DATASET.*" - } - // ... - } -} -``` - ## How to run Jenkins plugin in Debug mode in a local Jenkins sandbox For debugging purposes run following Maven command from plugin project directory: diff --git a/src/main/kotlin/org/zowe/zdevops/config/ZOSConnection.kt b/src/main/kotlin/org/zowe/zdevops/config/ZOSConnection.kt index e81e35d..ececb4b 100644 --- a/src/main/kotlin/org/zowe/zdevops/config/ZOSConnection.kt +++ b/src/main/kotlin/org/zowe/zdevops/config/ZOSConnection.kt @@ -8,7 +8,7 @@ * Copyright IBA Group 2022 */ -package org.zowe.zdevops.config; +package org.zowe.zdevops.config import com.cloudbees.plugins.credentials.CredentialsMatchers import com.cloudbees.plugins.credentials.CredentialsProvider diff --git a/src/main/kotlin/org/zowe/zdevops/declarative/AbstractZosmfAction.kt b/src/main/kotlin/org/zowe/zdevops/declarative/AbstractZosmfAction.kt index 7537d69..e985c1e 100644 --- a/src/main/kotlin/org/zowe/zdevops/declarative/AbstractZosmfAction.kt +++ b/src/main/kotlin/org/zowe/zdevops/declarative/AbstractZosmfAction.kt @@ -21,12 +21,9 @@ import hudson.tasks.BuildStepDescriptor import hudson.tasks.Builder import jenkins.tasks.SimpleBuildStep import org.zowe.kotlinsdk.zowe.client.sdk.core.ZOSConnection -import org.zowe.zdevops.Messages -import org.zowe.zdevops.config.ZOSConnectionList -import org.zowe.zdevops.utils.validateConnection +import org.zowe.zdevops.utils.getZoweZosConnection import java.io.PrintWriter import java.io.StringWriter -import java.net.URL import java.nio.charset.StandardCharsets abstract class AbstractZosmfAction : Builder(), SimpleBuildStep { @@ -39,20 +36,7 @@ abstract class AbstractZosmfAction : Builder(), SimpleBuildStep { override fun perform(run: Run<*, *>, workspace: FilePath, env: EnvVars, launcher: Launcher, listener: TaskListener) { val connectionName = workspace.read().readBytes().toString(StandardCharsets.UTF_8) - val connection = ZOSConnectionList.resolve(connectionName) ?: let { - - val exception = IllegalArgumentException(Messages.zdevops_config_ZOSConnection_resolve_unknown(connectionName)) - val sw = StringWriter() - exception.printStackTrace(PrintWriter(sw)) - listener.logger.println(sw.toString()) - throw exception - } - val connURL = URL(connection.url) - val zoweConnection = ZOSConnection( - connURL.host, connURL.port.toString(), connection.username, connection.password, connURL.protocol - ) - - validateConnection(zoweConnection) + val zoweConnection = getZoweZosConnection(connectionName, listener) runCatching { perform(run, workspace, env, launcher, listener, zoweConnection) diff --git a/src/main/kotlin/org/zowe/zdevops/declarative/ZosmfStepDeclarative.kt b/src/main/kotlin/org/zowe/zdevops/declarative/ZosmfStepDeclarative.kt index d3b3cfc..75349ed 100644 --- a/src/main/kotlin/org/zowe/zdevops/declarative/ZosmfStepDeclarative.kt +++ b/src/main/kotlin/org/zowe/zdevops/declarative/ZosmfStepDeclarative.kt @@ -13,17 +13,24 @@ package org.zowe.zdevops.declarative import hudson.EnvVars import hudson.Extension import hudson.FilePath -import hudson.model.* import hudson.model.Run +import hudson.model.TaskListener import org.jenkinsci.plugins.workflow.steps.Step import org.jenkinsci.plugins.workflow.steps.StepContext import org.jenkinsci.plugins.workflow.steps.StepDescriptor import org.jenkinsci.plugins.workflow.steps.StepExecution import org.kohsuke.stapler.DataBoundConstructor +import org.zowe.zdevops.utils.getZoweZosConnection +import org.zowe.zdevops.utils.validateConnection class ZosmfStepDeclarative @DataBoundConstructor constructor(private val connectionName: String) : Step() { override fun start(context: StepContext): StepExecution { + val listener: TaskListener? = context.get(TaskListener::class.java) + val zosConnection = getZoweZosConnection(connectionName, listener) + + validateConnection(zosConnection) + return ZosmfExecution(connectionName, context) } diff --git a/src/main/kotlin/org/zowe/zdevops/utils/ConnectionValidation.kt b/src/main/kotlin/org/zowe/zdevops/utils/ConnectionValidation.kt index d5211a0..110479f 100644 --- a/src/main/kotlin/org/zowe/zdevops/utils/ConnectionValidation.kt +++ b/src/main/kotlin/org/zowe/zdevops/utils/ConnectionValidation.kt @@ -11,10 +11,16 @@ package org.zowe.zdevops.utils import hudson.AbortException +import hudson.model.TaskListener import org.zowe.kotlinsdk.zowe.client.sdk.core.ZOSConnection import org.zowe.kotlinsdk.zowe.client.sdk.zosfiles.ZosDsnList import org.zowe.kotlinsdk.zowe.client.sdk.zosfiles.input.ListParams import org.zowe.zdevops.Messages +import org.zowe.zdevops.config.ZOSConnectionList +import java.io.PrintWriter +import java.io.StringWriter +import java.net.URL + /** * Gets a list of datasets @@ -39,4 +45,30 @@ fun validateConnection(zosConnection: ZOSConnection) { val connExMessage = "Failed to connect to z/OS (${zosConnection.user}@${zosConnection.host}:${zosConnection.zosmfPort}): ${connectException.message}" throw AbortException(connExMessage) } -} \ No newline at end of file +} + +/** + * Retrieves zOS connection by its name from the zOS connection list in Jenkins configuration. + * + * This function attempts to resolve the connection from the ZOSConnectionList + * using the provided connection name. If the connection is not found in the list, an + * IllegalArgumentException is thrown with a detailed error message, and the stack trace + * is logged using the provided TaskListener. + * + * @param connectionName The name of the connection to resolve. + * @param listener The TaskListener used to log messages and exceptions. + * @return A ZOSConnection object containing the resolved connection details. + * @throws IllegalArgumentException If the connection configuration cannot be resolved. + */ +fun getZoweZosConnection(connectionName: String, listener: TaskListener?): ZOSConnection { + val connection = ZOSConnectionList.resolve(connectionName) ?: run { + val exception = IllegalArgumentException(Messages.zdevops_config_ZOSConnection_resolve_unknown(connectionName)) + val sw = StringWriter() + exception.printStackTrace(PrintWriter(sw)) + listener?.logger?.println(sw.toString()) + throw exception + } + + val connURL = URL(connection.url) + return ZOSConnection(connURL.host, connURL.port.toString(), connection.username, connection.password, connURL.protocol) +}