:: krowemoh

Monday | 15 SEP 2025
Posts Links Other About Now

previous
next

Importing Linux Files into Pick

2025-09-14
pick, universe, sysadmin, scarletdme

Welcome to a hopefully short but thorough example of importing data into a Pick system.

The goal is to get /etc/passwd and /etc/group and their respective shadows into Pick so we can store copies of the Linux users inside Pick. This way I have an easy to use script to update new machines when we migrate. Currently I have a shell script that gets the users from one system and copies them to the new system. However I'm a big believer in moving as much to inside Pick as possible.

Pick in this case is a shorthand to say any Pick like system like UniVerse, UniData, D3, OpenQM or ScarletDME. I'm writing this example in ScarletDME while actually planning to use this tool in UniVerse.

There is a github repo with all the code.

Chapter 1 - The BASICs

Forgive the pun.

The first step is to read /etc/passwd and print it to the screen.

*
   OPENPATH '/etc' TO ETC.FILE ELSE
      PRINT 'Failed to open: /etc'
      STOP
   END
*
   READ PASSWD FROM ETC.FILE,'passwd' ELSE
      PRINT 'Failed to read: /etc/passwd'
      STOP
   END
*
   PRINT PASSWD
*
   STOP
*
* END OF PROGRAM
*
   END
*

The first step is to open the directory. Once we have a handler to the directory, we can then use a regular READ statement to read in the passwd file.

Pick does have ways of opening and reading files directly. You could use OPENSEQ and READSEQ with /etc/passwd to read the file but I prefer using the regular READ statement especially as I already know the Linux files are all plaintext.

If the files weren't plaintext, it would likely be better to use the sequential statements.

Once we have read the passwd file, we can then print it directly to the screen.

Now that we have a very simple program working, it's time to eat out vegetables.

We need to create the FILE that's going to hold our user data and set up our dictionary.

Chapter 2 - Some Light Reading

We first need to look at the structure of /etc/passwd. This will dictate how we create the dictionary.

We also need to look at /etc/shadow as I do want to store the password hashes as well.

There is a good article about the structure at cyberciti. You can also get the structure on the man pages.

The core structure is:

username:password:user_id:group_id:GECOS:directory:shell

Most of the fields are self-explanatory except for the GECOS. The GECOS field is a comment field that you store almost any information. It is free-form but usually it will hold extra information in a comma delimited way.

The password field will contain an x for most users as the real passwords are stored in the shadow file. This implies that at one point passwords may have been stored as plaintext in the passwd file. I'll need to do research on that to see if that's true.

Now for shadow, the core structure of it is:

username:password:last_changed:min_age:max_age:warning_period:inactivity_period:expiry_date

These fields are a bit more nuanced as they control different aspects of the password.

These are the fields you would modify to control how long passwords last, when they fully expire, when warnings to be shown and etc.

You can read more in the man page.

I want to store all the fields in pick so the first thing is to convert the above names to the Pick style:

USERNAME
PASSWORD
USER.ID
GROUP.ID
GECOS
DIRECTORY
SHELL
LAST.CHANGED
MIN.AGE
MAX.AGE
WARNING.PERIOD
INACTIVITY.PERIOD
EXPIRY.DATE

The PASSWORD field will hold the hash from shadow. Otherwise all the other fields willbe saved.

Chapter 3 - Dictionaries

Now that we know the structure of passwd and shadow and also defined the fields in the Pick style. Now we can create the file and set up the dictionaries.

I wrote a utility called ADD-DICT that lets you add dictionaries from TCl without editing the raw records.

I also usually use this utility inside a BASIC program so that setting up a file is a bit easier.

But first, let's create the file:

CREATE-FILE LINUX-USER-FILE

This will work in ScarletDME but for UniVerse you will need to supply some extra information:

CREATE-FILE LINUX-USER-FILE 1,11 11,1

I have NEW-FILE command that abstracts these extra parameters away and unifies creating files across the different systems.

NEW-FILE LINUX-USER-FILE

Now that we have the FILE created, we can now set up the dictionaries.

Create the following program called LINUX.USER.SCHEMA:

*
   EXECUTE 'CREATE-FILE LINUX-USER-FILE'
*
   EXECUTE 'ADD-DICT -F LINUX-USER-FILE -A 1 -N PASSWORD'
   EXECUTE 'ADD-DICT -F LINUX-USER-FILE -A 2 -J R -N USER.ID'
   EXECUTE 'ADD-DICT -F LINUX-USER-FILE -A 3 -J R -N GROUP.ID'
   EXECUTE 'ADD-DICT -F LINUX-USER-FILE -A 4 -N GECOS'
   EXECUTE 'ADD-DICT -F LINUX-USER-FILE -A 5 -N DIRECTORY'
   EXECUTE 'ADD-DICT -F LINUX-USER-FILE -A 6 -N SHELL'
   EXECUTE 'ADD-DICT -F LINUX-USER-FILE -A 7 -N LAST.CHANGED'
   EXECUTE 'ADD-DICT -F LINUX-USER-FILE -A 8 -N MIN.AGE'
   EXECUTE 'ADD-DICT -F LINUX-USER-FILE -A 9 -N MAX.AGE'
   EXECUTE 'ADD-DICT -F LINUX-USER-FILE -A 10 -N WARNING.PERIOD'
   EXECUTE 'ADD-DICT -F LINUX-USER-FILE -A 11 -N INACTIVITY.PERIOD'
   EXECUTE 'ADD-DICT -F LINUX-USER-FILE -A 12 -N EXPIRY.DATE'
*
   STOP
*
* END OF PROGRAM.
*
   END
*

This acts as a migration script to update the dictionaries. This script will set up all the dictionaries. These are all relatively simple entries as we dont have numbers or real dates. We will store everything as it was stored in the Linux files.

We don't have the username as a dictionary item because the username will be the key to the record.

We use the -J option to set the justification. The USER.ID and GROUP.ID are numeric and I want to be able to sort on them. This means that we need to make sure the justification is set. If we had dates or if the numbers needed some conversion, we could also specific the conversion by using -CV.

Chapter 4 - The Fun Stuff

Now with the structure studied and the file created, we can import some records.

The import is going to be simple because the Linux files are plaintext and use : as delimiters, it's already very analogous to the Pick way of doing things. Even the GECOS field which uses commas as delimiters means that we can convert those to regular value marks.

*
   EQU TRUE TO 1
   EQU FALSE TO 0
*
* COMPILER DIRECTIVES
*
   $DEFINE DATABASE.QM
   $DEFINE PLATFORM.LINUX
*
   $IFDEF DATABASE.QM
      $CATALOGUE LOCAL
   $ENDIF
*
* %INCLUDE LINUX-USER-FILE
*
   OPEN '','LINUX-USER-FILE' TO LINUX.USER.FILE ELSE
      PRINT 'Failed to open file: LINUX-USER-FILE'
      STOP
   END
*
   DIM LINUX.USER.ITEM(12)
   MAT LINUX.USER.ITEM = ''
*
   EQU LINUX.USER.PASSWORD.ATTRIBUTE TO 1
   EQU LINUX.USER.USER.ID.ATTRIBUTE TO 2
   EQU LINUX.USER.GROUP.ID.ATTRIBUTE TO 3
   EQU LINUX.USER.GECOS.ATTRIBUTE TO 4
   EQU LINUX.USER.DIRECTORY.ATTRIBUTE TO 5
   EQU LINUX.USER.SHELL.ATTRIBUTE TO 6
   EQU LINUX.USER.LASt.CHANGED.ATTRIBUTE TO 7
   EQU LINUX.USER.MIN.AGE.ATTRIBUTE TO 8
   EQU LINUX.USER.MAX.AGE.ATTRIBUTE TO 9
   EQU LINUX.USER.WARNING.PERIOD.ATTRIBUTE TO 10
   EQU LINUX.USER.INACTIVITY.PERIOD.ATTRIBUTE TO 11
   EQU LINUX.USER.EXPIRY.DATE.ATTRIBUTE TO 12
*
   @USER1 = 'IMPORT.LINUX.USERS'
   @USER2 = 'IMPORT.LINUX.USERS'
*
* %END
*
   OPENPATH '/etc' TO ETC.FILE ELSE
      PRINT 'Failed to open: /etc'
      STOP
   END
*
   READ PASSWD FROM ETC.FILE,'passwd' ELSE
      PRINT 'Failed to read: /etc/passwd'
      STOP
   END
*

The first half of the program should look familiar. It's the same OPEN and READ statements from before but now we have some constants also set up.

This will make the program much easier to read.

The big thing of note is that I use ITEM to mark dimensioned arrays and use the equates to access the fields within each item.

This makes things clear about what fields are being set and what each field actually means.

Now for the core logic:

   NUMBER.OF.LINES = DCOUNT(PASSWD,@AM)
*
   FOR I = 1 TO NUMBER.OF.LINES
      LINE = PASSWD<I>
*
      CONVERT ':' TO @AM IN LINE
      CONVERT ',' TO @VM IN LINE
*
      USERNAME = LINE<1>
*
      MAT LINUX.USER.ITEM = ''
*
      LINUX.USER.ITEM(LINUX.USER.USER.ID.ATTRIBUTE) = LINE<3>
      LINUX.USER.ITEM(LINUX.USER.GROUP.ID.ATTRIBUTE) = LINE<4>
      LINUX.USER.ITEM(LINUX.USER.GECOS.ATTRIBUTE) = LINE<5>
      LINUX.USER.ITEM(LINUX.USER.DIRECTORY.ATTRIBUTE) = LINE<6>
      LINUX.USER.ITEM(LINUX.USER.SHELL.ATTRIBUTE) = LINE<7>
*
      PRINT 'Importing: ' : USERNAME
      MATWRITE LINUX.USER.ITEM ON LINUX.USER.FILE, USERNAME
   NEXT I
*
   STOP
*
* END OF PROGRAM
*
   END
*

The READ statement gave use the passwd file contents. The READ statement automatically converts newlines to attribute marks. This is why we can count the number lines by counting the attribute marks.

The next step is to loop through the lines. We convert the : and , to attribute marks and value marks. This way we can then reference parts of the line numerically.

We then set up the LINUX.USER.ITEM and populate it with the values.

If I had wanted to be more explicit, I would set up equates for the passwd structure as well.

Something like:

EQU PASSWD.USERNAME.ATTRIBUTE TO 1

so that I can do:

LINE<PASSWD.USERNAME.ATTRIBUTE>

For our purposes though, I think it's fine to just go numerically as there aren;t too many fields. If there were more or if the program was much longer, I'd set up the equates.

There is nothing worse than being 500 lines deep and not remembering what field 47 means and how it relates to field 92.

Now we can run the program and we should have imported all of the users from `/etc/passwd.

If we do a listing of the LINUX-USER-FILE, we should see the following:

> LIST LINUX-USER-FILE username

LINUX-USER-F : username
PASSWORD.... :
USER.ID..... : 1000
GROUP.ID.... : 1000
GECOS....... :
DIRECTORY... : /home/username
SHELL....... : /usr/bin/fish
LASt.CHANGED :
MIN.AGE..... :
MAX.AGE..... :
WARNING.PERI :
INACTIVITY.P :
EXPIRY.DATE. :

With that we have the first file being imported.

Now let's finish it off by importing in /etc/shadow. One thing to note is that only root can read shadow so you will need to run the BASIC program as root actually import shadow.

   READ SHADOW FROM ETC.FILE,'shadow' ELSE
      PRINT 'Failed to read: /etc/shadow'
      STOP
   END
*
   NUMBER.OF.LINES = DCOUNT(SHADOW,@AM)
*
   FOR I = 1 TO NUMBER.OF.LINES
      LINE = SHADOW<I>
*
      CONVERT ':' TO @AM IN LINE
      CONVERT ',' TO @VM IN LINE
*
      USERNAME = LINE<1>
*
      MATREADU LINUX.USER.ITEM FROM LINUX.USER.FILE, USERNAME THEN
*
         LINUX.USER.ITEM(LINUX.USER.PASSWORD.ATTRIBUTE) = LINE<2>
         LINUX.USER.ITEM(LINUX.USER.LAST.CHANGED.ATTRIBUTE) = LINE<3>
         LINUX.USER.ITEM(LINUX.USER.MIN.AGE.ATTRIBUTE) = LINE<4>
         LINUX.USER.ITEM(LINUX.USER.MAX.AGE.ATTRIBUTE) = LINE<5>
         LINUX.USER.ITEM(LINUX.USER.WARNING.PERIOD.ATTRIBUTE) = LINE<6>
         LINUX.USER.ITEM(LINUX.USER.INACTIVITY.PERIOD.ATTRIBUTE) = LINE<7>
         LINUX.USER.ITEM(LINUX.USER.EXPIRY.DATE.ATTRIBUTE) = LINE<8>
*
         PRINT 'Importing shadow: ' : USERNAME
         MATWRITE LINUX.USER.ITEM ON LINUX.USER.FILE, USERNAME
*
      END ELSE
         RELEASE LINUX.USER.FILE, USERNAME
         PRINT 'Failed to read user: ' : USERNAME
         STOP
      END
   NEXT I
*

We first read in the shadow file and count of the number of lines.

The difference here is that we attempt to read the user from the LINUX.USER.FILE as it should already be there due to us having already imported passwd.

We read in the record with MATREADU so that we hold the update lock, this way no one can write to the record while we are working on it.

We populate the remaining fields using the shadow entry and then we write it out.

If for some reason the record doesn't already exist, then we do a RELEASE statement to release the lock we grabbed and then we do a hard stop. This will ultimately be a deeper investigation as it means passwd and shadow aren't in sync.

After this we can do another listing:

LINUX-USER-F : username 
PASSWORD.... : $y$j9T$O3u1/916rmY/S9YdmX
               Sa1a$aKaTafTad25aooW3Qr1k
               mahvAiKgYwr/QGVvb8LDjtA
USER.ID..... : 1000
GROUP.ID.... : 1000
GECOS....... :
DIRECTORY... : /home/username
SHELL....... : /usr/bin/fish
LAST.CHANGED : 20314
MIN.AGE..... : 0
MAX.AGE..... : 99999
WARNING.PERI : 7
INACTIVITY.P :
EXPIRY.DATE. :

With that we are done! We have now imported the passwd and shadow files into Pick.

We can add some smarts to clear the file and re-import things or we can leave it as it is and keep track of users that get removed from Linux.

We could run this routine everytime the Linux files change. However I'm going to keep it as a manual process.

> CLEAR-FILE DATA LINUX-USER-FILE
> RUN BP IMPORT.LINUX.USERS

With that done, the next post will cover exporting data from the LINUX-USER-FILE.