Skip to content

Commit

Permalink
Implement Perform MVS command method (#90)
Browse files Browse the repository at this point in the history
* remove 'WithResult' postfix

Signed-off-by: Anatoli Kalbasin <[email protected]>

* implement PerformMvsCommand method

Signed-off-by: Anatoli Kalbasin <[email protected]>

* add tests

Signed-off-by: Anatoli Kalbasin <[email protected]>

* update license

Signed-off-by: Anatoli Kalbasin <[email protected]>

* add docs

Signed-off-by: Anatoli Kalbasin <[email protected]>

---------

Signed-off-by: Anatoli Kalbasin <[email protected]>
  • Loading branch information
callbacksin authored Oct 22, 2024
1 parent 620a967 commit 95f7d6c
Show file tree
Hide file tree
Showing 11 changed files with 350 additions and 2 deletions.
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,47 @@ deleteDataset dsn:"EXAMPLE.DATASET"
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'.
```
### `performMvsCommand` - Execute an MVS System Command
The output of a command can be returned to a variable and subsequently processed. The step must be accompanied by `script` tag. It gives wide range of options such displaying system activities, device statuses, managing configuration, starting system tasks, etc.
#### Usage:
The step can be specified in two ways:
```groovy
performMvsCommand "DISPLAY TIME"
```
or using the named parameter:
```groovy
performMvsCommand command: "DISPLAY TIME"
```
**Mandatory Parameters:** there is only one parameter - `command`.

#### Example - Displaying Active Units of Work:
To display detailed information about all active units of work, use the following command:
```groovy
def active_units = performMvsCommand "D A,L"
```

#### Expected behavior under various scenarios:

* Insufficient Authorization: you are not authorized to issue the command:
```
[Perform MVS command] - Issuing command : D T
[Perform MVS command] - MVS command execution failed
Also: org.jenkinsci.plugins.workflow.actions.ErrorAction$ErrorId: f3f36e14-75f5-48ec-a47e-32727371972b
java.lang.Exception: {"reason":"Unexpected IEE136I: IEE345I DISPLAY AUTHORITY INVALID, FAILED BY SECURITY PRODUCT","return-code":5,"reason-code":4}
```
* Successful Execution:
```
[Perform MVS command] - Issuing command : D A,L
CNZ4105I 12.45.44 DISPLAY ACTIVITY 535
...
[Perform MVS command] - The command has been successfully executed
```
## 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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright (c) 2024 IBA Group.
*
* 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
*
* Contributors:
* IBA Group
* Zowe Community
*/

package org.zowe.zdevops.classic.steps

import hudson.Extension
import hudson.Launcher
import hudson.model.AbstractBuild
import hudson.model.BuildListener
import hudson.util.FormValidation
import org.kohsuke.stapler.DataBoundConstructor
import org.kohsuke.stapler.QueryParameter
import org.zowe.kotlinsdk.zowe.client.sdk.core.ZOSConnection
import org.zowe.zdevops.Messages
import org.zowe.zdevops.classic.AbstractBuildStep
import org.zowe.zdevops.logic.performMvsCommand
import org.zowe.zdevops.utils.validateFieldIsNotEmpty


/**
* A Jenkins Pipeline step for performing a MVS command on a z/OS system via freestyle job.
*/
class PerformMvsCommandStep
/**
* Data-bound constructor for the {@code PerformMvsCommandStep} step.
*
* @param connectionName The name of the z/OS connection to be used for executing the MVS command.
* @param command The MVS command to be executed.
*/
@DataBoundConstructor
constructor(
connectionName: String,
val command: String,
) : AbstractBuildStep(connectionName) {

/**
* Performs the MVS command execution step within a Jenkins Pipeline build.
*
* @param build The current Jenkins build.
* @param launcher The build launcher.
* @param listener The build listener.
* @param zosConnection The z/OS connection to execute the MVS command.
*/
override fun perform(
build: AbstractBuild<*, *>,
launcher: Launcher,
listener: BuildListener,
zosConnection: ZOSConnection
) {
performMvsCommand(zosConnection, listener, command)
}


/**
* Descriptor for the {@code PerformMvsCommandStep} step.
*
* This descriptor provides information about the step and makes it available for use
* within Jenkins Pipelines.
*/
@Extension
class DescriptorImpl : Companion.DefaultBuildDescriptor(Messages.zdevops_classic_performMvsCommandStep_display_name()) {

/**
* Performs form validation for the 'command' parameter to ensure it is not empty.
*
* @param command The MVS command field value to validate.
* @return A {@link FormValidation} object indicating whether the field is valid or contains an error.
*/
fun doCheckCommand(@QueryParameter command: String): FormValidation? {
return validateFieldIsNotEmpty(command)
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2024 IBA Group.
*
* 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
*
* Contributors:
* IBA Group
* Zowe Community
*/

package org.zowe.zdevops.declarative.jobs

import hudson.AbortException
import hudson.EnvVars
import hudson.Extension
import hudson.FilePath
import hudson.model.TaskListener
import org.kohsuke.stapler.DataBoundConstructor
import org.zowe.kotlinsdk.zowe.client.sdk.core.ZOSConnection
import org.zowe.zdevops.declarative.AbstractZosmfActionWithResult
import org.zowe.zdevops.logic.performMvsCommand

/**
* Class that represents an action to perform an MVS command with a result in a declarative pipeline.
* This class extends {@code AbstractZosmfActionWithResult} and is designed to execute an MVS command
* via Zowe z/OSMF and return the command's output.
*
* @param command the MVS command to be executed.
*/
class PerformMvsCommandDeclarative
@DataBoundConstructor
constructor(
val command: String,
) : AbstractZosmfActionWithResult() {

override fun run(
workspace: FilePath,
listener: TaskListener,
envVars: EnvVars,
zoweZOSConnection: ZOSConnection
): String {
return performMvsCommand(zoweZOSConnection, listener, command)
?: throw AbortException("MVS command execution returned an empty result")
}

@Extension
class DescriptorImpl : Companion.DefaultStepDescriptor(functionName = "performMvsCommand")
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,5 @@ constructor(
}

@Extension
class DescriptorImpl : Companion.DefaultStepDescriptor(functionName = "performTsoCommandWithResult")
class DescriptorImpl : Companion.DefaultStepDescriptor(functionName = "performTsoCommand")
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,5 @@ constructor(val fileToSubmit: String)
}

@Extension
class DescriptorImpl : Companion.DefaultStepDescriptor(functionName = "submitJobSyncWithResult")
class DescriptorImpl : Companion.DefaultStepDescriptor(functionName = "submitJobSync")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2024 IBA Group.
*
* 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
*
* Contributors:
* IBA Group
* Zowe Community
*/

package org.zowe.zdevops.logic

import hudson.AbortException
import hudson.model.TaskListener
import org.zowe.kotlinsdk.zowe.client.sdk.core.ZOSConnection
import org.zowe.kotlinsdk.zowe.client.sdk.zosconsole.ConsoleResponse
import org.zowe.kotlinsdk.zowe.client.sdk.zosconsole.IssueCommand

/**
* Executes an MVS command on a z/OS system using the provided z/OS connection.
*
* This function allows you to send an MVS command to a z/OS system, and capture the response
*
* @param zosConnection The z/OS connection through which the MVS command will be executed.
* @param listener The Jenkins build listener for logging and monitoring the execution.
* @param command The MVS command to be executed.
* @return the command output.
* @throws AbortException if the MVS command execution fails, with the error message indicating
* the reason for the failure.
*/
fun performMvsCommand(
zosConnection: ZOSConnection,
listener: TaskListener,
command: String,
): String? {
listener.logger.println("[Perform MVS command] - Issuing command : $command")
val commandResponseObj: ConsoleResponse
try {
commandResponseObj = IssueCommand(zosConnection).issueSimple(command)
listener.logger.println(commandResponseObj.commandResponse)
} catch (ex: Exception) {
listener.logger.println("[Perform MVS command] - MVS command execution failed")
throw ex
}
listener.logger.println("[Perform MVS command] - The command has been successfully executed")
return commandResponseObj.commandResponse
}
1 change: 1 addition & 0 deletions src/main/resources/org/zowe/zdevops/Messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ zdevops.classic.downloadDatasetStep.display.name=[z/OS] - Download dataset/membe
zdevops.classic.deleteDatasetStep.display.name=[z/OS] - Delete dataset/member
zdevops.classic.deleteDatasetsByMaskStep.display.name=[z/OS] - Delete datasets by mask
zdevops.classic.performTsoCommandStep.display.name=[z/OS] - Perform TSO command
zdevops.classic.performMvsCommandStep.display.name=[z/OS] - Perform MVS command
zdevops.declarative.ZOSJobs.submitting=Submitting a JOB from file {0} with connection: {1}:{2}
zdevops.declarative.ZOSJobs.submitted.success=JOB submitted successfully. JOBID={0}, JOBNAME={1}, OWNER={2}.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>

<?jelly escape-by-default='true'?>

<jelly:jelly xmlns:jelly="jelly:core" xmlns:form="/lib/form">
<form:entry field="connectionName" title="${%zdevops.classic.connection.title}">
<form:select/>
</form:entry>
<form:entry field="command" title="${%zdevops.classic.command.title}">
<form:textbox/>
</form:entry>
</jelly:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
zdevops.classic.connection.title=z/OS connection
zdevops.classic.command.title=MVS command to be executed
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright (c) 2024 IBA Group.
*
* 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
*
* Contributors:
* IBA Group
* Zowe Community
*/

package org.zowe.zdevops.classic.steps

import hudson.EnvVars
import hudson.FilePath
import io.kotest.assertions.assertSoftly
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.zowe.kotlinsdk.zowe.client.sdk.core.ZOSConnection
import org.zowe.zdevops.MOCK_SERVER_HOST
import org.zowe.zdevops.MockResponseDispatcher
import org.zowe.zdevops.MockServerFactory
import org.zowe.zdevops.declarative.jobs.PerformMvsCommandDeclarative
import java.io.File
import java.io.PrintStream
import java.nio.file.Paths

class PerformMvsCommandDeclarativeSpec : ShouldSpec({
lateinit var mockServer: MockWebServer
lateinit var responseDispatcher: MockResponseDispatcher
val mockServerFactory = MockServerFactory()

beforeSpec {
mockServer = mockServerFactory.startMockServer(MOCK_SERVER_HOST)
responseDispatcher = mockServerFactory.responseDispatcher
}
afterSpec {
mockServerFactory.stopMockServer()
}
context("declarative/jobs module: PerformMvsCommandStep") {
val zosConnection = ZOSConnection(mockServer.hostName, mockServer.port.toString(), "test", "test", "https")
val trashDir = tempdir()
val trashDirWithInternal = Paths.get(trashDir.absolutePath, "test_name").toString()
val workspace = FilePath(File(trashDirWithInternal))
val env = EnvVars()

afterEach {
responseDispatcher.removeAllEndpoints()
}
should("perform PerformMvsCommandDeclarative operation and return its result") {
var isPreExecuteStage = false
var isCommandExecuted = false
val expectedPartialMvsDisplayActiveCommandOutput = "CNZ4105I 12.56.27 DISPLAY ACTIVITY"
val taskListener = object : TestBuildListener() {
override fun getLogger(): PrintStream {
val logger = mockk<PrintStream>()
every {
logger.println(any<String>())
} answers {
if (firstArg<String>().contains("Issuing command")) {
isPreExecuteStage = true
} else if (firstArg<String>().contains("The command has been successfully executed")) {
isCommandExecuted = true
}
}
return logger
}
}

responseDispatcher.injectEndpoint(
this.testCase.name.testName,
{ it?.requestLine?.contains("PUT /zosmf/restconsoles/consoles/") ?: false },
{ MockResponse()
.setResponseCode(200)
.setBody(responseDispatcher.readMockJson("displayActiveASCommandOutput") ?: "") }
)

val performMvsCommandInst = spyk(
PerformMvsCommandDeclarative("D A,L")
)
val mvsCommandResult = performMvsCommandInst.run(workspace, taskListener, env, zosConnection)

assertSoftly { mvsCommandResult shouldContain expectedPartialMvsDisplayActiveCommandOutput }
assertSoftly { isPreExecuteStage shouldBe true }
assertSoftly { isCommandExecuted shouldBe true }
}
}
})
6 changes: 6 additions & 0 deletions src/test/resources/mock/displayActiveASCommandOutput.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"cmd-response-key": "C1977656",
"cmd-response-url": "https://192.168.1.1:10443/zosmf/restconsoles/consoles/defcn/solmsgs/C1977656",
"cmd-response-uri": "/zosmf/restconsoles/consoles/defcn/solmsgs/C1977656",
"cmd-response": " CNZ4105I 12.56.27 DISPLAY ACTIVITY 546\r JOBS M/S TS USERS SYSAS INITS ACTIVE/MAX VTAM OAS\r 00020 00040 00004 00034 00025 00002/00040 00041\r ..."
}

0 comments on commit 95f7d6c

Please sign in to comment.