Skip to content

IBM zOS and OS 400 Support

Robin Rodricks edited this page Feb 9, 2023 · 38 revisions

FluentFTP has extensive support for all variants of IBM z/OS and OS/400 Servers.

All credits for the development and testing of our excellent IBM z/OS API go to Michael Stiemke.

z/OS, also historically known as MVS, is only one of IBM's four mainframe operating systems (z/OS, z/VM, z/VSE, z/TPF).

IBM OS/400

One of the big stumbling blocks for accessing IBM OS/400 FTP servers is that, in a similar fashion to z/OS servers, there are a number of different listing formats that can be configured.

Beyond the somewhat strange naming schemes and the capability to directly access databases instead of just files, there is significantly less complexity to handle.

Most users in the past have probably used the library/filename format, which is more or less the default on these systems. Currently FluentFTP will not set Fullname correctly in this mode. Users will use FtpListOptions.Nopath, having navigated directly to the library where they plan to list files and up/download files.

If you want to have more control, consider reading this and perhaps using the SITE command to set LISTFMT to 1. In some cases this can improve your GetListing experience.

IBM z/OS

API

GetFileSize()

Returns the true file size in bytes for any z/OS file system object.

GetZOSListRealm()

Returns a value of the enum FtpZOSListRealm to describe the z/OS realm of the current working directory.

Return values can be:

  • FtpZOSListRealm.Invalid - Current server type and OS are not z/OS.

  • FtpZOSListRealm.Unix when the current working directory is in the unix (HFS / USS) realm.

  • FtpZOSListRealm.Dataset when the current working directory is in the general z/OS dataset realm. Note: This value is returned both for directories that represent an existing dataset ( which is then by nature sequential (DSROG PS)) or a directory that is not yet fully qualified .

  • FtpZOSListRealm.Member The current working directory fully qualifies a partitioned dataset. You could now list the members.

  • FtpZOSListRealm.MemberU The current working directory fully qualifies a partitioned dataset and it is a RECFM=U dataset. You could now list the members.

IsRoot

Check if the current working directory is the root directory of the file system. (Lexical checks are done to determine it)

Example:

client.SetWorkingDirectory("'GEEK.PRODUCT.LLIB'");
bool isr = client.IsRoot();

Realms

There are 2 realms that FluentFTP supports, and here is some example code for both realms:

Native z/OS realm:

string zosPath = "'SYS1.'"; // you can also use 'SYS1', both work

client.SetWorkingDirectory(zosPath);
FtpListItem[] list = client.GetListing("", FtpListOption.NoPath);
string fullName = zosPath.Trim('\'').Trim('.') + '.' + list[0].FullName.Remove(0, 1);

USS (linux-like) realm:

string zosPath = "/projects";

FtpListItem[] list = client.GetListing(zosPath);
string fullName = list[0].FullName;

Directory Listings

Filesystems

In z/OS, using the Communication Server for z/OS, the FTP server can LIST the following types of entries:

  • Classic z/OS datasets (many still refer to these as MVS datasets).
  • Datasets in other mounted filesystems. These are often referred to as HFS (Hierarchical File System) files, some people refer to these as files in USS (Unix System Services).
  • Members of classic z/OS partitioned non-RECFM=U datasets
  • Members of classic z/OS partitioned RECFM=U datasets (load libraries)

Listings

When you call GetListing, there are 4 types of listing that are implemented in order to handle variants of the z/OS filesytems:

  • List of USS files, is unix-like and handled by the standard FluentFTP unix parser
  • List of normal z/OS datasets (non VSAM)
  • List of members of partitioned datasets (non load library)
  • List of members of partitioned datasets (load library)

This IBM document documents the 4 different output formats of the LIST (or DIR) command.

Example code

The following test program demonstrates the differences between the various z/OS filesytems:

FtpClient client = new FtpClient();
client.Host = "ftps://xxxxxxx.xxxxx.com";
client.Credentials.UserName = "myuser";
client.Credentials.Password = "secret";
client.ValidateAnyCertificate = false;
client.ValidateCertificate += new FtpSslValidation(OnValidateCertificate);

client.AutoConnect();

// Member Listing non loadlib
string zosPath = "'MYUSER.PRODUCT.CLIB'";
client.SetWorkingDirectory(zosPath);
FtpListItem[] list1 = client.GetListing("", FtpListOption.NoPath);

// Member listing loadlib
zosPath = "'SYS1.V1R3M0.SEAGALT'";
client.SetWorkingDirectory(zosPath);
FtpListItem[] list2 = client.GetListing("", FtpListOption.NoPath);

// Dataset listing
zosPath = "'MYUSER.'";
client.SetWorkingDirectory(zosPath);
FtpListItem[] list3 = client.GetListing("", FtpListOption.NoPath);

// USS file listing
zosPath = "/projects";
FtpListItem[] list4 = client.GetListing(zosPath);

client.Disconnect();

How do I use GetListing?

Alternative #1

You can use GetListing() with an empty path and with FtpListOption.NoPath, which causes FluentFTP to use the LIST command without any further parameters - the zOS FTP Server accepts this in the z/OS realm and in the HFS / USS realm.

This in turn means that you must SetWorkingDirectory() to go to the desired working directory before doing the GetListing("", FtpListOption.NoPath.

string zosPath = "'MYUSER.PRODUCT.CLIB'";
client.SetWorkingDirectory(zosPath);
FtpListItem[] list1 = client.GetListing("", FtpListOption.NoPath);

Alternative #2

You can use GetListing() with a z/OS realm path and without FtpListOption.NoPath. If the dataset exists, you will get a list with one entry. Otherwise your list will be empty.

To list all datasets below a partly qualified z/OS realm path, you can append *. To list all members of a partitioned dataset, append (*).

Samples

// HFS with path
FtpListItem[] Listu1 = client.GetListing("/projects");

// HFS with NoPath option
client.SetWorkingDirectory("/projects");
FtpListItem[] Listu2 = client.GetListing("", FtpListOption.NoPath);

// z/OS with path
FtpListItem[] Lista1 = client.GetListing("PRODUCTS.*");
FtpListItem[] Lista2 = client.GetListing("PRODUCTS.CLIB(*)");
FtpListItem[] Lista3 = client.GetListing("PRODUCTS.CLIB");
FtpListItem[] Lista4 = client.GetListing("'GEEK.PRODUCTS.*'");
FtpListItem[] Lista5 = client.GetListing("'GEEK.PRODUCTS.CLIB(*)'");
FtpListItem[] Lista6 = client.GetListing("'GEEK.PRODUCTS.CLIB'");

// z/OS with NoPath option
client.SetWorkingDirectory("'GEEK'");
client.SetWorkingDirectory("PRODUCTS");
FtpListItem[] Listb1 = client.GetListing("", FtpListOption.NoPath);
client.SetWorkingDirectory("PRODUCTS.CLIB");
FtpListItem[] Listb2 = client.GetListing("", FtpListOption.NoPath);

.FullName is now set correctly in all these scenarios.

Additional notes

The IBM CS FTP server default behaviour/setup as configured might make GetListing(...) fail. Make sure the following settings are in effect:

DIRECTORYMODE FALSE QUOTESOVERRIDE TRUE

These settings are the default. You could change them in the FTP server configuration and if this is not possible for you, you can set these in the FTP session dynamically.

You want:

client.Execute("SITE DATASETMODE")
client.Execute("SITE QUOTESOVERRIDE")

You can check if these settings are in effect by using a STAT command. Search for these lines in the answer:

stat      
 >>> STAT
...
211-Data set mode.  (Do not treat each qualifier as a directory.)
...
211-Single quotes will override the current working directory.
...

FluentFTP will attempt to configure these settings on connection automatically if it detects the IBM CS FTP z/OS server.

It will also try to set SITE LISTLEVEL=0 (if supported) to get away from any SITE LISTLEVEL=1 that might be active, and then if supported, try to set SITE LISTLEVEL=2, again if supported. Once again, you can check the current setting yourself by issuing a STAT command.

In case your IBM CS FTP server reports that is supports features like SIZE, MDTM and UTF8, this is fine but be aware that these features are only relevant to FTP actions against unix (HFS) files.

How do I set up host file paths?

Use this to set up host file paths from GetListing()results: Note that this example uses Alternative #1

FtpClient client = new FtpClient();
client.Host = "ftps://xxxxxxx.xxxxx.com";
client.Credentials.UserName = "myuser";
client.Credentials.Password = "secret";
client.ValidateAnyCertificate = false;
client.ValidateCertificate += new FtpSslValidation(OnValidateCertificate);

client.AutoConnect();

string FTP_Hostfile;

string cwd = "'GEEK.PRODUCTS.LOADLIB'"; // Known to exist, PDS

client.SetWorkingDirectory(cwd);  // Position to there

FtpListItem[] entries = client.GetListing("", FtpListOption.NoPath);

FtpListItem entry = entries[0]; // as an example, take the first entry. 

if (cwd[0] == '\'') // z/OS realm
{
	// Check entry type. If it is not "Dataset", it must be Member or MemberU
	if (client.GetZOSListRealm() != FtpZOSListRealm.Dataset) //
		 // remove quotes, add "(membername from GetListing())" and requote
		FTP_Hostfile = "'" + cwd.Trim('\'') + "(" + entry.Name + ")'";
	else
		// remove quotes, add "(name from GetListing())" and requote
		FTP_Hostfile = "'" + cwd.Trim('\'') + entry.Name + "'";

}
else // HFS / USS realm
	FTP_Hostfile = cwd + entry.Name;

// and now do something with FTP_Hostfile, perhaps download or upload

client.Disconnect();

How do I find out which directory I am in?

To find out where you are, use GetWorkingDirectory().

string pwd = client.GetWorkingDirectory();

// pwd examples:
// 'GEEK.PRODUCTS.LOADLIB' This dataset exists.
// 'GEEK.PRODUCTS.' This is not an existing dataset. Note the trailing period - it tells us this fact
// /projects This is a HFS / USS path. Note there are no single quotes, but a slash in front always

How do I check if I am at the root directory?

To check if you are already "up at the top", use IsRoot(). It knows about z/OS dataset names and would consider a name such as 'SYS1.' to be a root. Any HLQ (high level qualifier on its own is considered to be root.

You can use SetWorkingDirectory("..") from such a root, but there no way to get a listing from there.

How do I detect which realm my current working directory (CWD) is in?

Get the result of a PWD and check the first character. If it is a single quote, it is the classic z/OS dataset realm, otherwise expect unix-like paths and filenames.

You can use GetZOSListRealm() to do exactly that. You will be given FtpZOSListRealm.Unix, FtpZOSListRealm.Dataset, FtpZOSListRealm.Member or FtpZOSListRealm.MemberU

Note that this tells you how the z/OS GetListing "directory" parser would parse the listing if you were to call GetListing("", FtpListOption.NoPath) at that location. So a result of FtpZOSListRealm.Member does not mean that the current working directory is a member (of a PDS). It rather means, you will get a list of members and that the current working directory is a partitioned dataset.

How do I get the file size?

Since the z/OS CS FTP server does not support the SIZE command to get the file size, here is a quick way to get it (only works if the host file is known to exist):

long size = client.GetFileSize("'GEEK.PRODUCTS.LOADLIB(FLXWTO)'");

It handles all the slash, single-quote, parentheses mangling that is needed for z/OS realms. This file size is subject to the same caveats as noted in the file size table further down.

Note that there is a reason for a missing indication of an exact actual file size information in native z/OS file systems (realms): It is a totally different architecture, even down to the physical disk layout. As stated elsewhere, the actual amount of bytes consumed when downloading such a z/OS dataset can not be predicted accurately by any other way than actually downloading it or reading it host-side. To repeat: This is a totally different approach to file allocation, reserve extents, compression, block and record organisation and in the end, you get a totally different concept of actual data use as a percentage of reserved and thus space unavailable to others. You allocate a dataset with certain constraints, and then use this space to the extent you decide on, under the limits you constrained yourself to upon allocation. You have been given this allocation and may use as much of it as you please. You are billed the allocation, not the actual use of it. So who cares how full it is?

How is the file size internally calculated?

The code will supply a size for each entry according to the following table:

HFS (or USS) realm: Size is correct.

z/OS (aka MVS) datasets: PS (i.e. non VSAM, non PDS, just sequential): Size is calculated by multiplying the used tracks by 3390-bytes-per-track (56664 bytes). Depending on the RECFM (consider for example V, VB, VBA), and/or compression, this might overestimate the size by a large amount. Best results on RECFM F, FB, FBA.

PO (i.e. Partitioned Dataset): Size is calculated by multiplying the used tracks by 3390-bytes-per-track (56664 bytes). Depending on the RECFM (consider for example V, VB, VBA), and/or compression, this might overestimate the size by a large amount. Best results on RECFM F, FB, FBA.

Members of a partitioned dataset:

non-RECFM=U (for example: RECFM=FB, very common): Size is returned as "number of records" multplied by LRECL. Depending on the RECFM (consider for example V, VB, VBA), and/or compression, this might overestimate the size by a large amount. Best results on RECFM F, FB, FBA.

LRECL is determined by issuing an internal XDSS command (see below).

Note that these file sizes depend on ISPF stats stored in the directory of the PDS for each member. These may be missing if the member was created by a non ISPF program. The size will then be reported as being zero. To create stats for such members, use a host-side program that invokes ISPF lib mngmt. service LMSTATS.

RECFM=U: Size is returned from the hex length field of the load library listing format and is correct

How do I check if a file exists?

Please refer to PR #765

How does GetListing() find the LRECL?

Not tricky at all: Since the introduction of MVSPUT and MVSGET FTP subcommands (from z/OS 2.1 onwards), there is a virtually undocumented command XDSS, which is needed by the functionality of these new commands.

Here is an example:

XDSS 'GEEK.TESTPROG.ASM'
200-LASTREF=2021/10/18 DSEMPTY=FALSE
200 SITE PDSTYPE=PDSE RECFM=FB BLKSIZE=16000 DIRECTORY=1 LRECL=80 PRIMARY=3 SECONDARY=110 TRACKS EATTR=SYSTEM

Note that you will very likely not be able to enter the XDSS FTP subcommand at your favorite ftp client. If you want test it, you need to use it from your FluentFTP session programatically, like in this snippet:

long LRECL;

if (entry.Type == FtpFileSystemObjectType.Directory)
{
	string pwd = GetPWD();
	if (pwd[0] == '\'')
	{
		LRECL = 0;
		FtpReply response = client.Execute("XDSS '" + pwd.Trim('\'') + entry.Name + "'");
		if (response.Success)
                        {
				// SITE PDSTYPE=PDSE RECFM=FB BLKSIZE=16000 DIRECTORY=1 LRECL=80 PRIMARY=3 SECONDARY=110 TRACKS EATTR=SYSTEM
				string[] words = response.Message.Split(' ');
				string[] val = words[5].Split('=');
				LRECL = Int64.Parse(val[1]);
			}
		InPDS = true;
	}
}

Accessing the JES2 internal reader to submit batch jobs and how to list and retrieve job output

When SITE FILETYPE=JES is active (as opposed to SITE FILETYPE=SEQ), a

  • STOR filename will upload a jcl file from your client system to the host and submit this as a job.
  • LIST will list the job output queue, from which you can select a job output to download to your client system
  • RETR jobname.n will then download the jobs output file number n.

Here is an example:

FTP_Sess.Config.ListingDataType = FtpDataType.ASCII;
FTP_Sess.Config.UploadDataType = FtpDataType.ASCII;
FTP_Sess.Execute("SITE FILETYPE=JES");
FTP_Sess.UploadFile("D:\\temp\\JES\\LISTCAT", "LISTCAT", FtpRemoteExists.NoCheck, false, FtpVerify.None);

assuming you have a file on your system with some jcl. This upload will transfer the jcl from the file to the JES2 internal reader and submit the job.

The LastReply object after the upload will look like this:

image

You will see, in the InfoMessages, there is 250-It is known to JES as JOBnnnnn.

So then you do:

FTP_Sess.Config.ListingDataType = FtpDataType.ASCII;
FTP_Sess.Config.DownloadDataType = FtpDataType.ASCII;
FtpStatus Jest = FTP_Sess.DownloadFile("D:\\temp\\JES\\LISTCAT.OUT.1", "JOB00061.1", FtpLocalExists.Overwrite, FtpVerify.None);

Here is the complete log of the process:

Command:  SITE FILETYPE=JES
Status:   Waiting for response to: SITE FILETYPE=JES
Response: 200 SITE command was accepted
>         UploadFile("D:\temp\JES\LISTCAT", "LISTCAT", NoCheck, False, None)
>         OpenWrite("LISTCAT", ASCII)
>         OpenPassiveDataStream(AutoPassive, "STOR LISTCAT", 0)
Command:  EPSV
Status:   Waiting for response to: EPSV
Response: 229 Entering Extended Passive Mode (|||1913|)
Status:   Connecting to ***:1913
Command:  STOR LISTCAT
Status:   Waiting for response to: STOR LISTCAT
Response: 125 Sending Job to JES internal reader FIXrecfm 80
Status:   Disposing FtpSocketStream...
Status:   Waiting for a response
Response: 250-It is known to JES as JOB00060
Response: 250 Transfer completed successfully.
>         DownloadFile("D:\temp\JES\LISTCAT.OUT.1", "JOB00061.1", Overwrite, None)
>         OpenRead("JOB00061.1", ASCII, 0, 0)
Command:  TYPE A
Status:   Waiting for response to: TYPE A
Response: 200 Representation type is Ascii NonPrint
>         OpenPassiveDataStream(AutoPassive, "RETR JOB00061.1", 0)
Command:  EPSV
Status:   Waiting for response to: EPSV
Response: 229 Entering Extended Passive Mode (|||1915|)
Status:   Connecting to ***:1915
Command:  RETR JOB00060.1
Status:   Waiting for response to: RETR JOB00061.1
Response: 125 Sending data set BFSYS.BFSYSA.JOB00061.D0000002.JESMSGLG
Status:   Disposing FtpSocketStream...
Status:   Waiting for a response
Response: 250 Transfer completed successfully.

You can also do:

FTP_Sess.Config.ListingDataType = FtpDataType.ASCII;
FtpListItem[] jesList = FTP_Sess.GetListing("LIST", FtpListOption.ForceList | FtpListOption.NoPath | FtpListOption.NoImage);
var listLine = jesList[0].Input;

which would give you in listLine: "BFSYSA JOB00069 BFSYS OUTPUT A RC=0004 4 spool files ". Parsing that will tell you there are 4 files, JOB00061.1 through JOB00061.4 that you could retrieve.

Clone this wiki locally