migrate-preferences   [plain text]


#! /usr/bin/env -i /usr/bin/ruby

##
# Copyright (C) 2007 Apple Inc. All rights reserved.
# 
# @APPLE_LICENSE_HEADER_START@
# 
# This file contains Original Code and/or Modifications of Original Code
# as defined in and that are subject to the Apple Public Source License
# Version 2.0 (the 'License'). You may not use this file except in
# compliance with the License. Please obtain a copy of the License at
# http://www.opensource.apple.com/apsl/ and read it before using this
# file.
# 
# The Original Code and all software distributed under the License are
# distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
# EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
# INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
# Please see the License for the specific language governing rights and
# limitations under the License.
# 
# @APPLE_LICENSE_HEADER_END@
##

ENV.clear
ENV['__CF_USER_TEXT_ENCODING'] = "0x#{Process::Sys::getuid()}:0:0"

require 'osx/foundation'
require 'optparse'

# NOTE Remember that this script is generally being called by either launchd or
# some admin tool. This means that we must implement a best-effort service
# model and never fail to do as much as possible. There will be no error output
# unless the --verbose flag is specified.

$VERBOSE = false    # Print trace messages
$RESTART = false    # Force service restart
$DEBUG = false	    # Print extra debugging statements

$ID = '$Id$'
$0 = File.basename($0)

OSX.require_framework 'SystemConfiguration'

# Wrapper class to access SCPreferences API.
class Preferences
    def initialize(appid)
	# Unlike the CFPreferences API, SCPreferences requires the actual
	# plist filename, which always end in '.plist'.
	appid = "#{appid}.plist" unless appid =~ /\.plist$/

	@prefs = OSX::SCPreferencesCreate(nil, $0, appid)
	@keys = Preferences.to_native(OSX::SCPreferencesCopyKeyList(@prefs))
	@keys.push('PreferencesSignature')

	$stderr.print \
	    "SCPreferences (appid=#{appid}) keys: #{@keys.join(",")}\n" \
	    if $DEBUG

    end

    def each
	keys.each { | key |
	    yield key, self[key]
	}
    end

    def signature
	sig = OSX::SCPreferencesGetSignature(@prefs)

	# Converting CFData to a string ends up with something that looks like
	# this: <0500000e 4a5e0f00 e8e24046 00000000 f8000000 00000000>
	# We strip the angle brackets and spaces to give a plain hex string.
	return sig.to_s.gsub(/[ <>]/, '')
    end

    def has_key?(key)
	return @keys.include?(key)
    end

    def [](key)
	case key
	when 'PreferencesSignature'
	    return self.signature
	else
	    val = OSX::SCPreferencesGetValue(@prefs, key)

	    # Need to convert to a native Ruby type because we merge these
	    # values with our default set, which are native types.
	    return (Preferences.to_native(val) rescue nil)
	end
    end

    def []=(key, val)
	$stderr.print "#{$0}: setting #{key} to #{val} (#{val.class})\n" \
	    if $VERBOSE

	OSX::SCPreferencesSetValue(@prefs, key, val)
    end

    def commit
	OSX::SCPreferencesCommitChanges(@prefs)
    end

    # Load a preferences hash from a plist.
    def Preferences.load_plist(path)
	print "#{$0}: loading #{path}\n" if $VERBOSE
	data = OSX::NSData.dataWithContentsOfFile(path)
	return nil unless data

	plist, format, err = OSX::NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription(data,
		OSX::NSPropertyListImmutable)

	if (plist == nil or !plist.kind_of? OSX::NSCFDictionary)
	    return nil
	end

	return Preferences.to_native(plist)
    end

    # Convert a CFPropertyListRef to a native Ruby type.
    def Preferences.to_native(val)

	return nil if val == nil

	$stderr.print "converting (#{val.class})\n" if $DEBUG

	if val.kind_of? OSX::NSCFBoolean
	    return (val == OSX::KCFBooleanTrue ? true : false)
	end

	if val.kind_of? OSX::NSCFString
	    return val.to_s
	end

	if val.kind_of? OSX::NSCFNumber
	    return val.to_i
	end

	if val.kind_of? OSX::NSCFArray
	    array = []
	    val.each { |element| array += [ Preferences.to_native(element) ] }

	    return array
	end

	if val.kind_of? OSX::NSCFDictionary
	    hash = {}
	    val.allKeys().each { | key |
		# Note: we need to convert both the key and the data, 
		# otherwise we will end up indexed by OSX::NSCFString and
		# won't be able to index by Ruby Strings.
		new_key = Preferences.to_native(key)
		new_val = Preferences.to_native(val[key])
		hash[new_key] = new_val
	    }
	    return hash
	end

	# NOTE: We don't convert CFData or CFDate because we
	# don't need them for the preferences we have.

	$stderr.print \
	    "#{$0}: preferences type #{val.class} is not supported\n" \
	    if $VERBOSE

	return nil
    end
end

class IniFile

    def initialize
	@hash = Hash.new()
    end

    def parse(path)
	current = nil

	File.open(path, 'r') { | fd |
	    fd.each_line { | line |
		line = extract(line)

		if line =~ /(\[.+\])/
		    current = $1
		    @hash[current] = Hash.new() if @hash[current] == nil
		    next
		end

		key, val = keyval(line)

		# print "section=#{current}, key=#{key}, val=#{val}\n" \
		#    if $VERBOSE

		next if key == nil
		next if current == nil

		@hash[current][key] = val
	    }
	}
    end

    def option(section, key)
	return (@hash[section]) ? @hash[section][key] : nil
    end

    def has_section(section)
	return (@hash[section]) ? true : false
    end

    def sections
	return @hash.keys
    end

    def write
	for section in @hash.keys
	    yield "#{section}\n"
	    for key in @hash[section].keys
		yield "\t#{key} = #{@hash[section][key]}\n"
	    end
	end
    end

private
    def extract(line)
	return line.chomp.sub(/[#;].*$/, '')
    end

    def keyval(line)
	line =~ /([^\s].*[^\s])\s*=\s*([^\s].*)\s*$/
	return [$1, $2]
    end
end

class Migrator
    def Migrator.migrate(conf, prefs)
	if $DEBUG
	    conf.write { | line | $stderr.print(line) }
	end

	netbios_name = conf.option('[global]', 'netbios name')
	if netbios_name
	    prefs['NetBIOSName'] = netbios_name
	end

	wins_server = conf.option('[global]', 'wins server')
	if wins_server
	    # We have to assume that a WINS server address also means we want
	    # to register with it because these are not separate options in
	    # smb.conf.
	    prefs['RegisterWINSName'] = true
	    prefs['WINSServerAddressList'] = 
		Migrator.list_to_array(wins_server)
	end

	workgroup = conf.option('[global]', 'workgroup')
	if workgroup
	    prefs['Workgroup'] = workgroup
	end

	realm = conf.option('[global]', 'realm')
	if realm
	    prefs['KerberosRealm'] = realm
	end

	server_desc = conf.option('[global]', 'server string')
	if workgroup
	    prefs['ServerDescription'] = server_desc
	end

	max_clients = conf.option('[global]', 'max smbd processes')
	if max_clients
	    prefs['MaxClients'] = max_clients.to_i
	end

	dos_charset = conf.option('[global]', 'dos charset')
	if dos_charset
	    prefs['DOSCodePage'] = dos_charset
	end

	passwd_server = conf.option('[global]', 'password server')
	if passwd_server
	    prefs['PasswordServer'] = passwd_server
	end

	log_level = conf.option('[global]', 'log level')
	if log_level
	    prefs['LoggingLevel'] = log_level.to_i
	end

	prefs['AllowLanManAuth'] =
	    Migrator.is_true(conf.option('[global]', 'lanman auth'))

	if conf.has_section('[homes]')
	    prefs['VirtualHomeShares'] = true
	end

	prefs['AllowGuestAccess'] = guest_access(conf)
	prefs['ServerRole'] = server_role(conf)
	prefs['EnabledServices'] = server_services(conf)
    end

private
    # Figure out which services are enabled
    def Migrator.server_services(conf)
	services = []

	for svc in conf.sections
	    case
		when svc.casecmp('[global]') == 0
		when svc.casecmp('[printers]') == 0
		else
		    services += ['disk']
		    break
	    end
	end

	if Migrator.is_true(conf.option('[global]', 'wins support'))
	    services += ['wins']
	end

	if conf.has_section('[printers]')
	    services += ['print']
	end

	return services
    end

    # Figure out the server role
    def Migrator.server_role(conf)
	security = conf.option('[global]', 'security')
	return 'Standalone' unless security != nil

	case
	    when security.casecmp('ads') == 0
		return 'ActiveDirectoryMember'

	    when security.casecmp('domain') == 0
		return 'DomainMember'

	    when security.casecmp('user') == 0
		# Either PDC, BDC or Standalone roles
		if Migrator.is_true(conf.option('[global]', 'domain logons'))
		    if Migrator.is_true(conf.option('[global]',
						    'domain master'))
			return 'PrimaryDomainController'
		    else
			return 'BackupDomainController'
		    end
		end

		return 'Standalone'

	    else
		# SHARE, SERVER security modes end up here
		return 'Standalone'
	end
	
    end

    # Figure out whether guest access is enabled
    def Migrator.guest_access(conf)
	guest = conf.option('[global]', 'map to guest')
	if guest == nil || guest.casecmp('never') == 0
	    return false
	else
	    return true
	end
    end

    # Split a list (strings separated by combinations of spaces and commas)
    # into an array of strings
    def Migrator.list_to_array(string)
	return string.split(/[\s,]+/)
    end

    def Migrator.is_true(string)
	return false unless string != nil
	return (string.casecmp('yes') == 0 || string == '1')
    end
end

opts = OptionParser.new
opts.banner = "Usage: #{$0} [options] FILES"
opts.on('--verbose',
'print extra debugging messages') {
    $VERBOSE = true
}

begin
    opts.parse!(ARGV) # Remove args as they are parsed.

    exit! 0 unless ARGV.length > 0

    prefs = Preferences.new('com.apple.smb.server')
    config = IniFile.new()

    # Collect all the options from the given list of smb.conf files.
    for path in ARGV
	config.parse(path)
    end

    Migrator.migrate(config, prefs)
    prefs.commit # Write prefs back to disk.

rescue OptionParser::InvalidOption => err
    $stderr.print "#{$0}: #{err}\n"
    $stderr.print opts.help()
    exit 1
rescue Exception => err
    if $VERBOSE
	$stderr.print "#{$0}: #{err}\n"
    end
    exit 2
end

# vim: filetype=ruby ai ts=8 sts=4 sw=4 tw=79