Friday, March 9, 2018

How Ivanti Handles Your macOS Package

Ivanti's software distribution client for macOS (sdclient) has some special built in behavior based on the file extension of the file being deployed. This article goes through each file type we support and explains what we will do with it.
This table contains a summary of the file extensions we will handle in a special way and what we will try to do with them.


File ExtensionAction
.dmg, .img, .smi, .iso Mount them then look for grab the first file and handle it recursively.
.zip, .bzip, .gz, .z, .tar, .tgzUncompress them and handle the contents recursively.
.pkg, .mpkgCall the command line version of the installer.
.sh Run the script as root.
.workflowRun with automator
.ldpatchThey should be a tgz file (renamed to a ldpatch file) with a subdirectory called Update and a script in the Update directory called postflight. We run postflight and clean up.
.mobileconfigInstall a configuration profile as the primary user of the machine
.prefPane (Case sensitive)Copy to /Library/PreferencePanes
.saverCopy to /Library/Screen Savers
.ttf or .dfontCopy to /Library/Fonts
Any other extensionCopy to /Applications

Conceptually, there are two classes of files we support; archives and files. This gets a little confusing since some .pkg and .mpkg files aren't files at all but are actually directories but, for the sake of simplicity, let's just call them files.
Archives are files that contain one or more arbitrary files. Things like zip files and .tgz files are good examples. We have taken one or more files and compressed them into a single file. Disk image files like .dmg files are also treated as archives. When sdclient sees an archive, it opens it up then looks for the first file contained in the archive. It then applies the appropriate action to that file. For example, if I have a .zip file that contains a .pkg file, sdclient would unzip the zip file into a temporary directory, find the .pkg since it is the first and only file in the zip, and run installer on it. If I have a single .app file in a .dmg, sdclient would mount the .dmg file, see the .app file, and copy it into /Applications. If, for some strange reason, I had a .zip file that contained a .dmg file that contained a .tff file, sdclient would unzip the zip file, see the .dmg file, mount the .dmg file, see the .tff file, and copy it into /Library/Fonts. Files are considered alphabetically. If a .dmg contained two files called installit.sh and application.app, installit.sh would get ignored and application.app would be moved into the "/Applications" folder.
All Apple .app files and older .pkg and .mpkg files are not actually files. They are folders. Finder hides this but have no doubt, you can not just copy on of these "files" to an http server and point an Ivanti SWD package at it. They must be contained in some sort of archive for distribution.
Keep in mind that sdclient is used for both software distribution and patch. If you are writing custom patches, you patch content will follow these same rules.
sdclient is a powerful tool. It integrates with Ivanti's Targeted Multicast service as well as peer to peer download and preferred server facilities. It also supports both http(s) and smb package sources. Hopefully, this short article will help you take advantage of sdclient's behaviors as you create your own packages and patches.

Wednesday, March 15, 2017

Create and Deploy a Local Scheduled Task With the Ivanti Local Scheduler

The Ivanti management agent for macOS provides a veritable plethora of tools that can be used by system administrators to get work done. One of the lesser known of these tools is the local scheduler or "ldscheduler." With ldscheduler, system administrators can schedule repeating tasks like generating custom inventory information or doing other system maintenance tasks without relying on the weight of the Ivanti core server's task scheduler.
In this series of articles, we will look at how to deploy a locally scheduled task from the core server. We will also take an exhaustive look at the many scheduling options ldscheuler provides.
Before we look at the details though, let's walk through a very basic scenario where we create a simple task, package it up, and deploy it using Ivanti.

Creating a Simple Task

ldscheduler's task description format is very loosely based on Microsoft's task scheduler schema. However, it has been extended to fill the needs of our agent. The most basic elements of a scheduler task are triggers and actions. Actions tell the scheduler what to do and triggers tell the scheduler when to do it.
A simple task would look something like this:
This task will pop up a dialog telling your users you care 10 seconds after the task is read into the local scheduler. Lines 7 - 10 establish a Time trigger with a Delay option. The Delay option uses ISO 8601 formatted duration notation. This delay is approximate depending on when the task is actually loaded into the scheduler and the processing load on the device.
Lines 12 - 17 define the action that is being scheduled. The "Command" tag takes a single shell command. This is not run through bash so things like the '~' and stream redirection will not work. The Context tag on line 15 tells the scheduler that this task should be run unprivileged as the current user. I will talk about other Context values in a future post.

Deploying the Task

To get a task into the scheduler, we need to get the XML file into the "/Library/Application Support/LANDesk/scheduler" directory. There are many ways to do this but the preferred way is to create a simple macOS package and deploy it as a software distribution task.
Creating a package on a Mac is simple. There are some GUI tools out there that you can use but for something this small, I would just use the 'pkgbuild' command line tool. First, make a directory.
mkdir task
The name of the directory does not matter but you need to have the .xml task file in a directory by itself. Let's assume I have copied the above .xml into a file called message.xml and saved it into a directory called task. I can now create a package using this command line:

pkgbuild --root task \
         --install-location "/Library/Application Support/LANDesk/scheduler" \
         message.pkg \
         --identifier com.ivanti.message

This will create a message.pkg file that can be installed on Macs. Running this installer will put the files that were in the "task" directory into "/Library/Application Support/LANDesk/scheduler". If you go ahead and open this file by double clicking on it, about 10 seconds after the install completes, a reassuring dialog box will pop up on your screen. Copy this file to a directory on your core server from which you distribute software. Now you can create an Ivanti software distribution package from the .pkg file and schedule it to be deployed to Macs owned by users who feel under-appreciated.
This article has covered the basic process of creating and deploying a simple local task to the Ivanti Mac local scheduler. In future articles, I will give in-depth descriptions of the various triggers, filters, and execution options available to system administrators.

Friday, January 20, 2017

But first, a word on Pipes

I was planning on this next article being about processing plist output from system_profiler but I decided to do a quick side trip to talk just a little bit about Pipes.
Swift's Pipe class is very similar to the '|' you would see in a bash script. In the previous article we hooked one end of the Pipe to a Process' stdout and our program read from it. Swift Pipes can also be used to connect running Processes together.
Consider the following code:
In lines 3 and 5 we create a pipe called lsStdout and hook it up to the stdout of the ls process. In line 12 we hook that same pipe to the stdin of the wc process. This feeds the list of files being generated by ls directly into the stdin of the wc command. By default, ls outputs one file name per line if it detects that it is not connected to a console so if we tell wc to count the lines of its stdin, we get a file count.
So the above code is basically the equivalent of this:
ls | wc -l

Or this in plain swift
let fm = try FileManager.default.contentsOfDirectory(atPath: "./") print(fm.count)

But that's not the point. The point is that pipes can be used to chain together processes much the same way as you can in bash. Maybe this is useful to you. Maybe it isn't. But I think it's sort of cool.
Next time I will take a look at generating and dealing with plist output from system_profiler.

Thursday, December 29, 2016

Running Tasks from Swift

For my day job, I write macOS management agent software for LANDESK. A significant part of this job is gathering the current state of Apple Macintosh Computers. Apple provides a system configuration framework that can provide much of the information I need. That framework has "documentation" that can be a challenge to decode. Many times, for quick prototypes, I will call out to the system_profiler command line tool. It gives quick feedback and you can easily see the layout of the information you are dealing with.
Apple's new Swift language has some great features that make calling external programs and digging through their output safe and relatively easy. In this post and in several to come, I will show you how to do this. Disclaimer: I would not consider this "best practices." The code in these posts was written while I was first learning Swift myself and is the result of several quick prototypes. Still, I think it is a decent example of using Swift to run external programs.
Running /usr/sbin/system_profiler will give you a whole lot of information about your Mac. In fact, everything you can get from the "System Information" app shows up in the text output of system_profiler. Since some of this information is gathered every time system_profiler is run, it can take a long time. Fortunately, we can ask system_profiler to just give us information on a specific sub-system by specifying a data type. For instance, we can get just network information or general information about our hardware. Running system_profiler SPHardwareDataType would give us this:
Hardware: 

    Hardware Overview:

      Model Name: iMac
      Model Identifier: iMac15,1
      Processor Name: Intel Core i7
      Processor Speed: 4 GHz
      Number of Processors: 1
      Total Number of Cores: 4
      L2 Cache (per Core): 256 KB
      L3 Cache: 8 MB
      Memory: 32 GB
      Boot ROM Version: IM151.0207.B06
      SMC Version (system): 2.22f16
      Serial Number (system): C02NN4G6FY14
      Hardware UUID: D402F211-62D7-59CA-84D0-F887EFF089DC

You can get a list of data types by running system_profiler -listDataTypes. Running system_profiler from Swift is simple. Swift has a Process class that will take care of the work of starting and managing an external task. The following launchAndGetText function can be copied and pasted directly into an Xcode playground. It will run a shell command found at path with the arguments found in the String array args and return the resulting output in a String. If no text output was generated by the command at path, it returns an empty string.

This function is pretty straightforward. Lines 2 through 4 set up the basic process object. The launchPath property on the process object "ps" is just the path of the program we want to run. I like to pass the full path, not just the program name, primarily for security reasons but also because I don't like to rely on the $PATH environment variable. The arguments property is just an array of strings that will be passed as command line arguments. If you have done any C programming for UNIX, you might be wondering about arg[0] and such. Don't. Just put the arguments to the program, not the name of the program itself into the arguments array. You can look at the example later in the post if this isn't making sense.
Lines 6 and 7 attach a Pipe to the Process object's standard out stream. This Pipe lets you read the bytes coming out of the running processes standard out stream. So anything the process normally prints to the terminal is available for your program to read. You can also hook up pipes to standard error to get error messages and standard in if you want to send data to the process. Lines 9 and 10 launch the process and wait for it to finish. There are ways to get the termination status from the process. We will take a look at that another time.
The gist that follows shows an example of using launchAndGetText to get the hardware UUID from a Mac.

You can see on lines 1 and 2 that we are calling system_profiler with a single command line argument, SPHardwareDataType. This will return the text output from system_profiler as a String. We can use the Swift String's component method to break the text into lines of text in an array of Strings. Finding the UUID is then a simple matter of walking through the array and looking for the string "Hardware UUID". Swift provides several ways of digging through arrays and I will spend several posts talking about them in the future. For now I just use the for in loop that is similar to what you would find in most programming languages.
This approach of grabbing the plain text output of system_profiler works great for simple information like the text we get using the SPHardwareDataType argument. Not all everything we might want to get out of system_profiler is that simple. For example, the output of SPNetworkDataType is hierarchical with a level of interfaces, each interface having various bits of information like interface names, addresses, proxy information, and so on. Here is an example of what you get for two interfaces:

    Wi-Fi:

      Type: AirPort
      Hardware: AirPort
      BSD Device Name: en0
      IPv4 Addresses: 192.168.0.4
      IPv4:
          AdditionalRoutes:
              DestinationAddress: 192.168.0.4
              SubnetMask: 255.255.255.255
          Addresses: 192.168.0.4
          ARPResolvedHardwareAddress: 58:8b:f3:ae:9d:c9
          ARPResolvedIPAddress: 192.168.0.1
          Configuration Method: DHCP
          ConfirmedInterfaceName: en0
          Interface Name: en0
          Network Signature: IPv4.Router=192.168.0.1;IPv4.RouterHardwareAddress=58:8b:f3:ae:9d:c9
          Router: 192.168.0.1
          Subnet Masks: 255.255.255.0
      IPv6:
          Configuration Method: Automatic
      DNS:
          Domain Name: Home
          Server Addresses: 192.168.0.1, 205.171.3.25
      DHCP Server Responses:
          Domain Name: Home
          Domain Name Servers: 192.168.0.1,205.171.3.25
          Lease Duration (seconds): 0
          DHCP Message Type: 0x05
          Routers: 192.168.0.1
          Server Identifier: 192.168.0.1
          Subnet Mask: 255.255.255.0
      Ethernet:
          MAC Address: 20:c9:d0:44:0e:a1
          Media Options: 
          Media Subtype: Auto Select
      Service Order: 1

This is a small subset of the network information that comes out of system_profiler. You get all this and more for each interface on your computer. Finding the MAC address for a specific interface becomes complicated using this unstructured output. Fortunately, system_profiler will also output information in Apple's plist format and Swift knows exactly what to do with that. In my next post, we will take a look at taking XML from system_profiler and putting it into Swift data structures that can be easily navigated.

Saturday, October 1, 2011

Thoughts on Weight Loss

About a year ago, I decided I would start counting calories to see if it would help me lose weight. I have been overweight or obese since I was in elementary school. Over the past 15 years or so, I have been fairly physically active. I ran consistently, played basketball, hiked, cycled, but never really lost weight. It didn't take long to figure out why. After setting up an account on myfitnesspal.com and entering a couple of days worth of calories, I found that I had usually used my day's worth of calories by lunch, even taking exercise into account. A morning fritter and a fast food lunch racked up calories pretty fast. So I started eating breakfast at home. I either took a lunch to work or came home for lunch. When I did eat out, I would lean towards less fatty food and I would bring food home rather than finish it at the restaurant. I took smaller portions, especially of dessert. I carefully tracked calories. As I lost weight, I found I could exercise more. I started training for the Riverton half marathon. By decreasing my calories and increasing my exercise, I started really losing weight. My goal was to get down around 200 pounds. I am at 176 now. If I go by body fat percentage rather than BMI, I am no longer overweight.
For the first time that I can remember in my life, I am not overweight.
Now I am faced with keeping the weight off. I can say that I have made some significant changes in habits that should help, but there are two things that I have noticed that I suspect will make it easy to fall back into old patterns if I am not diligent.
The first is that, when I look in the mirror, I don't look any different at 176 than I did at 230. I understand women who lose weight and still obsess over looking fat. There is an abundance of physical evidence that I don't look the same. None of my cloths come even close to fitting. I have gone from a 42 waist to a 34. But I see myself in the mirror every day so the change was very slow and subtle. So, when I look in the mirror, I look the same. This can be pretty frustrating. It doesn't appear to me that I have changed and I really miss my morning fritter. It would be very easy to slip back into that old bad habit. I think I will need to keep some of my old clothes around for a wile so that I can put them on and see that evidence that I truly have changed.
The second is that being overweight was part of my identity.  First and foremost, I was an archetype of a particular kind of computer programmer. It was how I was supposed to look for the role I played in life. In addition,  I was a "fat guy who ran". I was a "fat guy who skied". These made me a little unusual. Maybe a little special. Now I'm just a guy who runs, just a guy who skis. To keep the weight off, I need to replace the "fat guy" part of my identity with something else. So I have transitioned from being a guy who runs to being a runner. (I don't have enough money to become a skier.) There is a difference. A guy who runs goes out in the morning or on weekends and runs. A runner participates in organized events. A runner doesn't just go out and run, he trains. This takes up time. But without that next event, without that training schedule, without that piece of identity to replace "fat guy," I know I will fall back into old habits.
Both of these challenges were a big surprise. I am doing my best to find ways to overcome them. My last physical attests to the value of getting my weight under control. I just need to overcome my own perceptions and the pieces of my identity that have been established over 46 years. Should be a piece of cake. A small piece of cake. With maybe just one scoop of ice cream.