Using and Sharing Disk Resources with NFS

 < Day Day Up > 

Command-Line NetInfo Administration Tools

Not only does the NetInfo database have a graphical interface through the NetInfo manager, but there are also command-line tools for interacting with NetInfo. In this section, we will take a practical look at some of the tools by demonstrating the concept of using skeleton accounts to manage user accounts. Then we will take a brief look at other NetInfo command-line tools that you might find useful.

Creating Skeleton User Accounts

If you're going to have any significant number of users on your machine (or machines), you'll soon find that being able to provide a more customized environment than what comes out of the system Accounts control pane by default is a benefit.

Apple has provided a convenient method for you to perform some customization of accounts as created by the Accounts control pane. This is the inclusion of a User Template directory, from which the accounts made by the pane are created by duplication. The family of User Template directories, individualized by locale, are kept in /System/Library/User Template. This system works for simple configuration settings that you might like to configure for each newly created user, but it has some limitations if you want to work with more complex setups. The largest logical limitation is that if you're trying to set up complicated startup scripts and sophisticated environment settings, using a real user account as your default template is nice because you can log in for testing and tweaking. The largest practical limitation is that Apple has put the default templates in the /System/ hierarchy, where they're Apple-sacrosanct, and system updates are likely to tromp on any customizations that you might make.

The easiest way to solve all the problems at once is to create a skeleton user account as a real user account, and to keep it up-to-date with any environmental customizations that you want to provide for new users when you create accounts. If you create the skeleton user as simply another user account, you can log in to it and then conveniently tweak its settings. Using this method, you can create as many skeleton accounts as you need for different collections of settings.

Even if you prefer to use the Accounts System Preferences pane, the creation of skeleton users as real users on the system can be useful. You can configure skeleton users who you can actually log in as and test their settings, and then populate the /System/Library/User Template directories (if you don't mind incurring the wrath of the Apple installers), as required for customizing the configuration of users under the Users pane in System Preferences. Alternatively, you can create the accounts with Apple's default templates and then overwrite the actual user directories with data from your skeleton account.

As covered in Chapter 9, "Accessing the BSD Subsystem," every user's shell environment is configured by the .profile and .bashrc files (if the user is using bash), or .login and .cshrc (if the user's using tcsh or csh) shell scripts in the user's home directory. You might also want to provide a more customized starter web page or assorted bits of default data or files in the user's home directory. If you're managing student or employee accounts, you might have basic application preferences that you want to come preconfigured. Consider the wealth of personal customizations that you put into your own account. There are certainly many that other users aren't going to be interested in, but there are also undoubtedly many that would be useful starting places for other users on your system. There is also a lot of work involved in putting these preferences together, so despite the extra work involved in making well-customized user templates, you could be saving many hours of work for your users if you can leverage the work you've already put into the system. If you have many users, this can be a real productivity enhancer and time saver.

After you've configured an account in the fashion you want your new users to have, the hard part is done. It would be nice to have a way to use this account directly from the Users pane as the seed for new accounts as they are created, but, unfortunately, we aren't so lucky yet. Instead, you have two options for how to use the starter account information. First, you can create a new user through the Accounts pane. After the account has been created, you can replace the user's home directory (that the Accounts pane created) with a copy of the skeleton account home directory.

Your other option is to ignore the Accounts pane and create a new user by duplicating an existing user node from the NetInfo hierarchy, making a copy of the skeleton account home directory for the new user's home directory, and then editing the copy of the NetInfo entry for the new user to reflect the correct information for that user.

The first option is probably easier for novice users, but the second has the benefit of being able to be done from the command line with nidump and niload, and therefore, of being automatable. Table 20.1 contains command documentation for nidump. Table 20.2 contains command documentation for niload.

Table 20.1. The nidump Utility Can Export NetInfo Data to Plain Text Formats

nidump

Extracts text or flat file format data from NetInfo.

nidump [-t] { -r <directory> | <format> } <domain>

nidump reads the specified NetInfo domain and dumps a portion of its contents to standard output. When a flat file administration format is specified, nidump provides output in the syntax of the corresponding flat file. Allowed values for <format> are aliases, bootparams, bootptab, exports, fstab, group, hosts, networks, passwd, printcap, protocols, rpc, and services.

If -r is used, the first argument is interpreted as a NetInfo directory path, and its contents are dumped in a generic NetInfo format.

-t

Interprets the domain as a tagged name.

-r

Dumps the specified directory in raw format. Directories are delimited in curly brackets. Properties within a directory are listed in the form property = value;. Parentheses introduce a comma separated list of items. The special property name CHILDREN is used to hold a directory's children, if any. Spacing and line breaks are significant only within double quotes, which can be used to protect any names with meta characters.


Table 20.2. The Command Documentation Table for niload

niload

niload populates NetInfo directories with multiple properties at once.

niload [-v] [-d] [-m] [-p] [-t] {-r <directory> | <format>} <domain>

niload loads information from standard output into the specified NetInfo <domain>. If <format> is specified, the input is interpreted according to the flat-file format <format>. Acceptable values for <format> are aliases, bootparams, bootptab, exports, fstab, group, hosts, networks, passwd, printcap, protocols, rpc, and services.

If -r <directory> is specified instead of a flat-file format, the input is interpreted as raw NetInfo data, as generated by nidump -r, and is loaded into <directory>.

niload overwrites entries in the existing directory with those contained in the input. Entries that are in the directory, but not in the input, are not deleted unless -d is specified. niload must be run as the superuser on the master NetInfo server for <domain> unless -p is specified.

-v

Verbose mode. Prints + for each entry loaded, and - for each entry deleted (flat-file formats only).

-d

Deletes entries that already exist in the directory, but that aren't duplicated in the input.

-p

Prompts for the root password of the given domain so that the command can be run from locations other than the master.

-m

Merge into an existing NetInfo structure instead of overwriting in the case of collisions.

-u <user>

Authenticates as <user>. Implies -p.

-P <password>

Provides <password> on the command line. Overrides -p.

-t

Interprets the domain as a tagged domain. For example, TRotter/network refers to the domain network on the machine TRotter. Machine name can be specified as an actual name or an IP address.

-r

Loads entries in raw format, as generated by nidump -r. The first argument should be the path of a NetInfo directory into which the information is loaded. The specified directory may be renamed as a result of contents of the input, particularly if the input includes a top-level name property. If the specified directory does not exist, it is created.

<domain>

NetInfo <domain> that is receiving input. If . is the value for <domain>, it is referring to the local NetInfo database.


For the rest of the discussion, it is assumed that you've created a skeleton account in which you have made any customizations that you want to install for all new users. The account UID is assumed to be 5002, with a home directory of /Users/skel and a GID of 101. It is also assumed that you've added the group users to your NetInfo groups directory, with a GID of 101 (we've previously used 99 for this group, but Apple's put a system group on that ID with 10.3), and that you want to use this GID for normal, nonprivileged users. If you prefer to use Apple's scheme of having every user in a different, unique group, this method is adaptable to that as well.

To implement the first method of providing local customization for a new user, follow these steps:

1.

Create the new user with the Accounts pane. Make any necessary changes to the user's configuration, such as the default GID, using NetInfo Manager as shown in earlier chapters.

2.

Become root (su, provide password).

3.

Change directories to the skeleton user's directory (cd ~skel).

4.

Tar the contents of the current directory, using the option to place the output on STDOUT (tar -cf - .) and then pipe the output of tar into a subshell. In the subshell, cd to the new user's directory, and untar from STDIN (| ( cd ~<newusername> ; tar -xf - ) ).

NOTE

If you've created preferences in the skeleton user's account that rely on resource forks, you'll want to use the ditto command instead of tar or read ahead to the note regarding hfstar.


5.

Change directories to one level above the new user's directory (cd ~<newusername> ; cd ../).

6.

Change the ownership of everything in the new user's directory to belong to the new user and, potentially, to the user's default group if it's not the same as theskel account default group (chown -R <newusername>:<newusergroup> <newuserdirectoryname>). We'll cover the complete documentation for chownat the end of this chapter.

For example, if you've just created a new user named jim, assigned to the group users with the Accounts pane/NetInfo Manager, and want to put the skel account configuration into jim's home directory, you would enter the following:

 su (provide password) cd ~skel tar -cf - . | ( cd ~jim ; tar -xf - ) cd ~jim cd ../ chown -R jim:users jim 

If you'd rather create new users from the command line, either because you can't access the physical console conveniently or because you want to use what you know about shell scripting to automate the process, you can use the second method suggested earlier. You might find this method more convenient for creating users in a NetInfo domain other than localhost/local. The Accounts pane in the nonserver version of Mac OS X seems incapable of creating users in other NetInfo domains, and this makes using it for managing cluster users difficult.

CAUTION

This process creates a new user by manipulating the NetInfo database directly, so the cautions to back up your database frequently are important to remember here.


To implement the second method, follow these steps:

1.

Become root (su, give password).

2.

Change directories to the directory in which you want to place the new user's home directory (cd /Users, for example).

3.

Make a directory with the short name of the user you're about to create (mkdir <newusername> to create a directory for a new user named <newusername>).

4.

Change directories to the home directory of the skel account (cd ~skel).

5.

Tar the contents of the current directory and use the option to place the output on STDOUT (tar -cf - .).

6.

Pipe the output of the tar command into a subshell. In the subshell, cd to the new user's directory, and untar from STDIN (| ( cd <pathtonewuserdirectory> ; tar -xf - ). Note that you can't use ~<newusername> because <newusername> doesn't actually exist on the system yet.

7.

Dump your skel account (UID 5002 here, remember) NetInfo entry, or some other user's entry, into a file that you can edit (nidump -r /name=users/uid=5002 -t localhost/local > ~/<sometempfile>). As an alternative to the uid search, you could specify the skel account with /name=users/name=skel.

8.

Edit ~/<sometempfile>, changing the entries so that they are appropriate for the new user you want to create. You'll want to change at least _writers_passwd, _writers_tim_password, uid, _writers_hint, _writers_picture, gid, realname, name, passwd, and home. It's probably easiest to leave passwd blank for now.

NOTE

If you want to use the "unique group for every user" management paradigm that Apple has moved to (which frankly we think is a management nightmare), you'll want to change GID here as well.


9.

Use niutil to create a new directory for the uid that you've picked for the new user (niutil -p -create -t localhost/local /name=users/uid=<newuserUID>; give the root password when asked). Table 20.3 shows the command syntax and some of the most useful options for niutil.

Table 20.3. The Syntax and Popular Options for niutil

Niutil

The NetInfo Utility niutil is used to edit the NetInfo database.

niutil -create [opts] <domain> <path>

niutil -destroy [opts] <domain> <path>

niutil -createprop [opts] <domain> <path> <key> [<val>...]

niutil -appendprop [opts] <domain> <path> <key> <val>...

niutil -mergeprop [opts] <domain> <path> <key> <val>...

niutil -insertval [opts] <domain> <path> <key> <val> <index>

niutil -destroyprop [opts] <domain> <path> <key>

niutil -destroyval [opts] <domain> <path> <key> <val>

niutil -renameprop [opts] <domain> <path> <oldkey> <newkey>

niutil -read [opts] <domain> <path>

niutil -list [opts] <domain> <path>

niutil -readprop <domain> <path> <key>

niutil -readval <domain> <path> <key> <index>

niutil -rparent [opts] <domain>

niutil -resync [opts] <domain>

niutil -statistics [opts] <domain>

niutil -domainname [opts] <domain>

niutil performs arbitrary reads and writes on the specified NetInfo <domain>. To perform writes, niutil must be run as root on the NetInfo master for the database, unless -p, -P, or -u is specified. The directory specified by <path> is separated by / characters. A numeric ID may be used for a path in place of a string. Property names may be given in a path with an =. The default property name is name.

The following examples refer to a user with user ID 3:

/name=users/uid=3

/users/uid=3

-t <host>/<tag>

Interprets the domain as a tagged domain. For example, parrish/network is the domain tagged network on machine parrish.

-p

Prompts for the root password or the password of <user> if combined with -u.

-u <user>

Authenticates as <user>. Implies -p.

Operations

-create <domain> <path>

Creates a new directory with the specified path.

-destroy <domain> <path>

Destroys the directory with the specified path.

-createprop <domain> <path> <key> [<val>...]

Creates a new property in the directory <path>. <key> is the <key> [<val>...] name of the property. Zero or more property values may be specified. The property is created empty if no <val>s are provided. If the named property already exists, it is overwritten.

-appendprop <domain> <path> <key> <val>...

Appends new values to an existing property in directory <path>. <key> is the name of the property. Zero or more property values <key> <val>... may be specified. If the named property does not exist, it is created.

-insertval <domain> <path> <key> <val> <index>

Inserts a new value into an existing property in the directory <path> at position <index>. <key> is the name of the <key> <val> property. If the named property does not exist, it is created.

-destroyprop <domain> <path> <key>

Destroys the property with name <key> in the specified <path>.

-destroyval <domain> <path> <key> <val>

Destroys the specified value in the property named <key>in the specified <path>.

-read <domain> <path>

Reads the properties associated with the directory <path> in the specified <domain>.

-list <domain> <path>

Lists the directories in the specified <domain> and <path>. Directory IDs are listed along with directory names.

-readprop <domain> <path><path><key>

Reads the value of the property named <key> in the directory of the specified <domain>.

-readval <domain> <path> <key><index>

Reads the value at the given <index> of the named <keyproperty in the specified directory.

-rparent <domain>

Prints the current NetInfo parent of a server. The server should be explicitly given using the -t <host>/<tag> option.

-statistics <domain>

Prints server statistics on the specified <domain>.

-domainname <domain>

Prints the domain name of the given domain.


10.

Use niload to load the data you modified in ~/<sometempfile> back into the NetInfo database (cat ~/<sometempfile> | niload -p -r /name=users/uid=<newuserUID> -t localhost/local).

11.

Set the password for the new user (passwd <newusername>;). Provide a beginning password another BSD utility documented at the end of this chapter.

12.

Change back to the directory above the new user's home directory (cd ~<newusername>; cd ../).

13.

Change the ownership of the new user's directory to the new user's <username> and <defaultgroup> (chown -R <username>:<usergroup> <newuserdirectory>).

That might look like a lot of typing, but that's what shell scripts are for. If you have two users to create, that's a lot of typing. If you have 200 users, it's much less to type the script once and run it 200 times than to create each manually with the Accounts preferences pane and NetInfo Manager.

If you've made a mistake somewhere along the way, just restore your NetInfo database from the backup that you made before you started this. You also might need to find the nibindd process, and send it a HUP signal (\ps -auxww | grep "nibindd"; kill -HUP <whatever PID belongs to nibindd>, or, killall -HUP nibindd, if you prefer to do things the easy way).

RESOURCE FORKS GET LOST IN THE TAR!

The version of tar distributed by Apple doesn't understand file resource forks, and some software vendors haven't caught on to the idea of using plists properly yet. The unfortunate consequence is that if you've built a highly customized skeleton user (or are trying to use this as an example to move a real user account), and some of the user's preferences are stored in the resource fork of the preference files, tar is going to make a mess of things when you use it to duplicate the user's directory to the new location.

To overcome this problem, you currently have two options:

  • metaobject has developed hfstar, a GNUtar derivative that supports HFS+, allowing it to properly handle resource forks, type and creator codes, and so on. Because Apple is now distributing GNUtar instead of BSDtar, it's probably safe to do a straight-up replacement of Apple's tar with hfstar if you want (BSDtar and GNUtar have sufficient differences that outright replacement was not previously a good option).Instead of replacement, it would be better to keep both hfstar and Apple's tar around, just in case there turn out to be unexpected differences. Because metaobject has managed to get resource forks working with GNUtar, I'm hopeful that Apple will follow suit with an updated version of tar, obviating the need for replacement. metaobject's hfstar can be downloaded from http://www.metaobject.com/Products.html.

  • Use Apple's already supplied ditto command. ditto doesn't provide nearly the power of tar, but it'll do for copying user directories. More information on ditto is provided in Chapter 29, "Maintaining a Healthy System."


To produce results similar to those from the earlier method, the following example creates a new user with the username of james, UID 600, GID 70 (the web group www), with home directory /Users/james. This again assumes the skel account with UID 5002 and characteristics as described earlier.

 su (provide the password) cd /Users mkdir james cd ~skel tar -cf - . | ( cd /Users/james ; tar -xf - ) nidump -r /name=users/uid=5002 -t localhost/local > ~/skeltemp vi ~/skeltemp 

and change the contents from this:

 {   "hint" = ( "" );   "sharedDir" = ( "Public" );   "_writers_passwd" = ( "skel" );   "authentication_authority" = ( ";ShadowHash;" );   "name" = ( "skel" );   "home" = ( "/Users/skel" );   "passwd" = ( "********" );   "_writers_hint" = ( "skel" );   "_writers_picture" = ( "skel" );   "_shadow_passwd" = ( "" );   "realname" = ( "Skeleton User" );   "uid" = ( "5002" );   "shell" = ( "/bin/bash" );   "generateduid" = ( "66EA85A3-E1A9-11D7-9893-0030654C2E9C" );   "gid" = ( "101" );   "_writers_tim_password" = ( "skel" );   "picture" = ( "/Library/User Pictures/Animals/Jaguar.tif" );   "_writers_realname" = ( "skel" ); } 

to this:

 {   "authentication_authority" = ( ";basic;" );   "picture" = ( "/Library/User Pictures/Nature/Zen.tif" );   "_shadow_passwd" = ( "" );   "hint" = ( "boggle" );   "uid" = ( "600" );   "_writers_passwd" = ( "james" );   "realname" = ( "Sweet Baby James" );   "_writers_hint" = ( "james" );   "gid" = ( "70" );   "shell" = ( "/bin/bash" );   "name" = ( "james" );   "_writers_tim_password" = ( "james" );   "passwd" = ( "" );   "_writers_picture" = ( "james" ) ;   "home" = ( "/Users/james" );   "sharedDir" = ( "Public" ); } 

Then run these commands:

 niutil -p -create -t localhost/local /name=users/uid=600 (give the root password when asked) cat ~/skeltemp | niload -p -r /name=users/uid=600 -t localhost/local (give the root password when asked) passwd james (fill in a good starting value) cd ~james cd ../ chown -R james:www james (GID 70 is group www on this machine) 

NOTE

Depending on whether your NetInfo daemon is feeling well, you might have to HUP the nibindd process to get it to recognize that you've made the change. Remember that you can always restore your NetInfo database backup to get out of a mess, if you've created one.


TIP

If you need to delete a user account from the command line, you can destroy the NetInfo information for the user by using the command niutil -p -destroy -t localhost/local /name=users/uid=<userUIDtobedeleted>. Then \rm -rf the user's home directory to delete it and all its contents from the system.


Just to make sure that your user has been created as you think it should have been, you can use niutil to list the /users NetInfo directory. (Don't be surprised if your listing doesn't look quite like this this is simply the list of users configured on my machine, so your users are likely to be different.)

 brezup:root Users # niutil -list -t localhost/local /users 11       nobody 12       root 13       daemon 14       unknown 15       lp 16       postfix 17       www 18       eppc 19       mysql 20       sshd 21       qtss 22       cyrusimap 23       mailman 24       appserver 25       clamav 26       amavisd 27       jabber 28       xgridcontroller 29       xgridagent 30       appowner 31       windowserver 32       tokend 33       securityagent 92       ray 94       software 97       skel 99       james 101      testme 

As shown, james does now exist in the NetInfo /users directory, although this listing shows only the NetInfo node numbers, rather than the users and property values. To see whether james has the properties intended, you can use niutil to read the info from the node named james:

 brezup:root Users # niutil -read -t localhost/local /users/james hint: boggle sharedDir: Public _writers_passwd: james authentication_authority: ;ShadowHash; name: james home: /Users/james passwd: ******** _writers_hint: james _writers_picture: james _shadow_passwd:  realname: Sweet Baby James uid: 600 shell: /bin/bash gid: 70 _writers_tim_password: james picture: /Library/User Pictures/Animals/Jaguar.tif _writers_realname: james 

James can now log in and functions just like a user that you created through the Accounts pane.

NOTE

Frankly, we can't figure out what some values in the output from the nidump of the user information are for, or whether they affect the system by being present, absent, or changed. Some of these, such as the generateduid, seem likely to change as Apple matures the password authentication system. This value, for example, is an artifact of the way that Apple's chosen to overcome a security vulnerability in earlier implementations of the NetInfo database. It doesn't seem to actually be used for anything (or, rather, it seem to be a unique hash value generated to match between the user's information in NetInfo and an encrypted password stored in a file; it seems, however, to be automatically generated and replaced as needed), and nothing seems to break if we remove it. Unfortunately, the root cause is that Apple's new user password storage system makes a portion of the user authentication information local to each machine, instead of allowing it all to be cleanly served from a remote server. This probably means that Apple will supercede this method, and that the requirement for this value will be changed when Apple implements some secure yet networkable scheme in the future.


Additional Interesting NetInfo Command-Line Utilities

In the previous section, we showed you the NetInfo utilities we find to be the most useful. In this section we will look at nifind, nigrep, and nireport. Another utility that you might find useful, but we will not document, is nicl. It is another general NetInfo utility that you can use to perform activities similar to what we did in the previous section. Its most interesting feature is that it has an interactive mode for communicating with the NetInfo database.

The nifind utility, documented in Table 20.4, can be used to find a directory ID in the NetInfo database. For example, to see what the directory ID of the software user is, we could run

 brezup:ray ray $ nifind -a /users/software  /users/software found in ., id = 94 

Table 20.4. The Command Documentation Table for nifind

Nifind

Finds a directory in the NetInfo hierarchy.

nifind [ -anvp ] [ -t <timeout> ] <directory> [ <domain> ]

nifind searches for the named directory in the NetInfo hierarchy. It starts at the local domain and climbs up through the hierarchy until it reaches the root domain. Any occurrences of directory are reported by directory ID number. If the optional <domain> argument is given, nifind stops climbing at that point in the hierarchy. The <domain> argument must be specified by an absolute or relative domain name.

-a

Searches for <directory> in the entire NetInfo hierarchy.

-n

Exempts local directories from the search.

-p

Prints directory contents.

-t <timeout>

Specifies an integer value to use as the connection timeout.

-v

Produces verbose output.


This was information that we were also able to find by having niutil list the entire /users directory, but if you need the directory number for something, nifind is a convenient way to get it. You can also have nifind print the contents of the directory that it finds with the -p option, much like having niutil read a specific directory.

You can search for patterns in the NetInfo database by using nigrep, documented in Table 20.5. To find out which users have the pattern ja, you could run:

 brezup:ray ray $ nigrep ja . /users 27 /users/jabber:  name jabber 27 /users/jabber:  _writers_passwd jabber 99 /users/james:  _writers_passwd james 99 /users/james:  name james 99 /users/james:  home /Users/james 99 /users/james:  _writers_hint james 99 /users/james:  _writers_picture james 99 /users/james:  _writers_tim_password james 99 /users/james:  _writers_realname james 

Table 20.5. The Command Documentation Table for nigrep

nigrep

Searches for a regular expression in the NetInfo hierarchy.

nigrep <expression> [ -t ] <domain> [ <directory> ... ]

nigrep searches through the specified <domain> argument for a regular expression. It searches the domain's directory hierarchy depth-first starting from the root directory. It can also start from each directory specified on the command line.

On output, nigrep prints the directory ID number of the directory that contains the regular expression, and the property key and values where it was found. A line is printed for each property that contains the regular expression.

-t

Specifies the <domain> as a network address or a hostname and a tag.


Perhaps the most interesting of the utilities in this section is nireport, whose documentation is in Table 20.6. This utility can be used to provide much nicer views of the data contained in the NetInfo database.

Table 20.6. The Command Documentation Table for nireport

nireport

Prints tables from the NetInfo hierarchy.

nireport [ -t ] <domain> <directory> [ <property> ...]

nireport prints a table of values of properties in all subdirectories of the directory given on the command line. Multiple values of a property are printed in a comma-separated list.

-t

Specifies <domain> as a network address or hostname and tag.


Where you might use niutil to list the /users directory and then use it again to read properties in certain subdirectories, you could just use nireport to generate a table listing of all the properties you are interested in seeing from the subdirectories in /users. To quickly see the uid and gid for users, you could run:

 brezup:ray ray $ nireport . /users name uid gid nobody  -2      -2 root    0       0 daemon  1       1 unknown 99      99 lp      26      26 postfix 27      27 www     70      70 eppc    71      71 mysql   74      74 sshd    75      75 qtss    76      76 cyrusimap       77      6 mailman 78      78 appserver       79      79 clamav  82      82 amavisd 83      83 jabber  84      84 xgridcontroller 85      85 xgridagent      86      86 appowner        87      87 windowserver    88      88 tokend  91      91 securityagent   92      92 ray   501     501 software        502     100 skel    5002    101 james   600     70 testme  601     70 

     < Day Day Up > 


    Mac OS X Tiger Unleashed
    Mac OS X Tiger Unleashed
    ISBN: 0672327465
    EAN: 2147483647
    Year: 2005
    Pages: 251

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net