Finish Scripts and First-Boot Services with Bootable AI
By scottdickson on Dec 20, 2010
In general, there is no notion of an install script that is bundled into a package when using the Image Package System in Solaris 11 Express as there is with SVR4 packages from Solaris 10. However, a package can install a service that can carry out the same function. Its operation is a bit more controlled since it has to have its dependencies satisfied and can run in a reduced privilege environment if needed. Additionally, many of the actions typically scripted into installation scripts, such as creation of users, groups, links, directories, are all built-in actions of the packaging system.
So, the question arises of how to use the IPS packaging system to add our own packages to a system, whether at installation time or later, and how to perform the necessary first-boot customizations to a system we are installing. The requirement to create our own packages comes from the fact that there is no other way to deliver content to the system being installed during the installation process except through the AI manifest - and that means IPS packages. In Jumpstart, there was a variable set at installation that pointed to a particular NFS-mounted directory where install scripts could reside. This was all well and good so long as you could mount that directory. When it was not available, you were left again with the notion of creating and delivering SVR4 packages via the Jumpstart profile. So, the situation is not far different than in Solaris 10 and earlier. There's just a little different syntax and a different network protocol in use to deliver the payload.
Why Use a Local Repository
There are two main reasons to create a local repository for use by AI and IPS. First, you might choose to replicate the Solaris repository rather than make a home-run through your corporate network firewall to Oracle for every package installation on every server. Performance and control are clearly going to be better and more deterministic by locating the data closer to where you plan to use it. The second reason to create a local repository would be to host your own locally provided packages - whether developed locally or provided by an ISV.
The question then arises whether to combine both of these into the same repository. My personal opinion is that it is better to keep them separate. Just as it is a good practice to keep the binaries and files of the OS separate from those locally created or provided by applications on the disk, it seems a good idea to keep the repositories separate. That does not mean that multiple systems are needed to host the data, however. Multiple repository services can be hosted on the same system on different network ports, pointing to different directories on the disk. Think of it like hosting multiple web sites with different ports and different htdocs directories.
Rather than go through all the details of creating a local mirror of the Solaris repository, I will refer you to Brian Leonard's blog on that topic. Here, he talks about creating a local mirror from the repository ISO. He also shows how you can create a file-based repository for local use.
For much of the rest of this exercise, I am relying on an article in the OpenSolaris Migration Hub that talks about creating a First Boot Service, which in turn references a page on creating a local package repository. It is well worth it to read through these two pages.
Setting Up A Local Repository
So far, we have avoided the use of any sort of install server. However, to have a local repository available during installation, this becomes a necessity. So, pick a server to be used as a package repository.
Rather than clone the Solaris repository, we will create a new, empty repository to fill with our own packages. We will configure the necessary SMF properties to enable the repository. And then we will fill it with the packages that we need to deploy.
On the host that will host the repository, select a location in its filesystem and set it aside for this function. A good practice would be to create a ZFS filesystem for the repository. In this case, you can enable compression on the repository and easily control its size through quotas and reservations. In this example, we will just create a ZFS filesystem within the root pool. Often you will have a separate pool for this sort of function.
# zfs create -p -o mountpoint=/var/repos/mycompany.com/repo -o compression=on rpool/repos/mycompany.com/repo
Next, we will need to set the SMF properties to support the repository. The service application/pkg/server is responsible for managing the actual package depot process. As such, it refers to properties to locate the repository on disk, establish what port to use, etc.
The property pkg/inst_root specifies where on the repository server's disk the repo resides.
# svccfg -s application/pkg/server setprop pkg/inst_root=/rpool/repo0906/repo
pkg/readonly specifies whether or not the repository can be updated. Typically, for a cloned Solaris repository, this will be set to true. It also is a good practice to set it to true when it should not be updated.
# svccfg -s application/pkg/server setprop pkg/readonly=true
pkg/prefix specifies the name that the repository will take when specified as a publisher by a client system. pkg/port specifies the port where the repository will answer.
# svccfg -s application/pkg/server setprop pkg/prefix=local-pkgs
# svccfg -s application/pkg/server setprop pkg/port=9000
Once the properties are set, refresh and enable the service.
# svcadm refresh application/pkg/server
# svcadm enable application/pkg/server
Creating a First-Boot Package
Now that the repository has been created, we need to create a package to go into that repository. Since there are no post-install scripts with the Image Packaging System, we will create an SMF service that will be automatically enabled so that it will run when the system boots. One technique used with Jumpstart was to install a script into /etc/rc3.d that would run late in the boot sequence and would then remove itself so that it would only run on the first boot. We will take a similar path with our first-boot service. We will have it disable itself so that it doesn't continue to run on each boot. There are two parts to creating this simple package. First, we have to create the manifest for the service, and second, we have to create the script that will be used as the start method within the service. This area is covered in more depth in the OpenSolaris Migration Hub paper on Creating a First Boot Service. In fact, we will use the manifest from that paper.
Creating the Manifest
We will use the manifest from Creating a First Boot Service directly and call it finish-script-manifest.xml. The main points to see here are
- the service is enabled automatically when we import the manifest
- the service is dependent on svc:/milestone/multi-user so that it won't run until the system is in the Solaris 11 Express equivalent of run level 3.
- the script /usr/bin/finish-script.sh, which we will provide, is going to be run as the start method when the service begins.
<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1">
<service_bundle type='manifest' name='Finish:finish-script'>
<create_default_instance enabled='true' />
<dependency name='autofs' grouping='require_all' restart_on='none' type='service'>
<service_fmri value='svc:/system/filesystem/autofs:default' />
<dependency name='multi-user' grouping='require_all' restart_on='none' type='service'>
<service_fmri value='svc:/milestone/multi-user:default' />
<property_group name='startd' type='framework'>
<propval name='duration' type='astring' value='transient' />
Creating the Script
In this example, we will create a trivial finish script. All it will do is log that it has run and then disable itself. You could go so far as to have the finish script uninstall itself. However, rather than do that we will just disable the service. Certainly, you could have a much more expansive finish script, with multiple files and multiple functions. Our script is short and simple:
svcadm disable finish-script
#pkg uninstall pkg:/finish
echo "Completed Finish Script" > /var/tmp/finish_log.$$
Adding Packages to Repository
Now that we have created the finish script and the manifest for the first-boot service, we have to insert these into the package repository that we created earlier. Take a look at the pkgsend man page for a lot more details about how all of this works. It's possible with pkgsend to add SVR4 package bundles into the repository, as well as tar-balls and directories full of files. Since our package is simple, we will just insert each package.
When we open the package in the repository, we have to specify the version number. Take a good look at the pkg(5) man page to understand the version numbering and the various actions that could be part of the package. Since I have been working on this script, I have decided that it is version 0.3. We start by opening the package for insertion. Then, we add each file to the package in the repository, specifying file ownership, permissions, and path. Once all the pieces have been added, we close the package and the FMRI for the package is returned.
# eval `pkgsend -s http://localhost:9000/ open email@example.com`
# pkgsend -s http://localhost:9000/ add file finish-script-manifest.xml mode=0555 owner=root group=bin path=/var/svc/manifest/system/finish-script-manifest.xml restart_fmri=svc:/system/manifest-import:default
# pkgsend -s http://localhost:9000/ add file finish-script.sh mode=0555 owner=root group=bin path=/usr/bin/finish-script.sh
# pkgsend -s http://localhost:9000/ close
Updating the AI Manifest
Now that the repository is created, we can add the new repository as a publisher to verify that it has the contents we expect. On the package server, itself, we can add this publisher. Remember that we set the prefix for the publisher to local-pkgs and that we specified it should run on port 9000. This name could be anything that makes sense for your enterprise - perhaps the domain for the company or something that will identify it as local rather than part of Solaris is a good choice.
# pkg set-publisher -p http://localhost:9000 local-pkgs
Added publisher(s): local-pkgs
# pkg list -n "pkg://local-pkgs/\*"
NAME (PUBLISHER) VERSION STATE UFOXI
finish (local-pkgs) 0.3 known -----
Now that we know the repository is on-line and contains the packages that we want to deploy, we have to update the AI manifest to reflect both the new publisher and the new packages to install. First, to update the publisher, beneath the section that specifies the default publisher, add a second source for this new publisher:
Then, add the packages to the list of packages to install. Depending on how you named your package, you may not need to specify the repository for installation. IPS will search the path of repositories to find each package. However, it's not a bad idea to be specific.
<software_data action="install" type="IPS">
Now, when you install the system using this manifest, in addition to the regular Solaris bits being installed, and the system configuration services executing, the finish script included in your package will be run on the first boot. Since the script called by the service turns itself off, it will not continue to run on subsequent boots. You can do whatever sort of additional configuration that you might need to do. But, before you spend a long time converting all of your old Jumpstart scripts into a first-boot service, take a look at the built-in capabilities of AI and of Solaris 11 Express in general. It may be that much of the work you had to code yourself before is no longer required. For example, IP interface configuration is simple and persistent with ipadm. Other new functions in Solaris 11 Express remove the need to write custom code, too. But for the cases where you need custom code, this sort of first-boot service gives you a hook so that you can do what you need.