Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
481 lines (387 sloc) 17.1 KB
###############################################################################
# Copyright (c) 2008-2009 VMware, Inc.
#
# This file is part of Weasel.
#
# Weasel is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation version 2 and no later version.
#
# Weasel is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# version 2 for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
#
import os
import re
import random
import string
import crypt
import shutil
import migrate
import userchoices
from util import execWithRedirect, execWithLog
from log import log
from consts import HOST_ROOT, ESX3_INSTALLATION
from exception import InstallationError
import workarounds
from xml.dom import minidom
SALT_CHARS = string.letters + string.digits + './'
ALLOWED_CHARS = string.digits + string.ascii_letters + string.punctuation + " "
ALLOWED_USER_CHARS = string.digits + string.ascii_letters
RESERVED_ACCOUNTS = ['root', 'bin', 'daemon', 'adm', 'lp', 'sync', 'shutdown',
'halt', 'mail', 'news', 'uucp', 'operator', 'gopher',
'ftp', 'nobody', 'nscd', 'vcsa', 'rpc', 'sshd',
'rpcuser', 'nfsnobody', 'pcap', 'vimuser', 'ntp', 'rpm',
'sys', 'games', 'proxy', 'www-data', 'backup', 'list',
'irc', 'gnats', 'syslog', 'klog', 'messagebus',
'avahi-autuipd', 'avahi', 'cupsys', 'haldaemon',
'hplip', 'gdm', 'statd', 'man', 'dhcp']
HOSTD_AUTHORIZATION = "etc/vmware/hostd/authorization.xml"
USER_XML_TEMPLATE = """
<ACEData id="%(id)s">
<!-- generated by weasel/users.py -->
<ACEDataEntity>ha-folder-root</ACEDataEntity>
<ACEDataId>%(id)s</ACEDataId>
<ACEDataIsGroup>false</ACEDataIsGroup>
<ACEDataPropagate>true</ACEDataPropagate>
<ACEDataRoleId>-1</ACEDataRoleId>
<ACEDataUser>%(uname)s</ACEDataUser>
</ACEData>
"""
NEXT_AUTH_ID = "<NextAceId>%s</NextAceId>"
def sanityCheckPassword(password):
if len(password) < 6:
raise ValueError, "Passwords must be at least 6 characters long."
if len(password) > 64:
raise ValueError, "Passwords must be less than 64 characters long."
for letter in password:
if letter not in ALLOWED_CHARS:
raise ValueError, "Passwords may only contain ascii characters."
def sanityCheckUserAccount(account):
if not account:
raise ValueError, "You need to specify a user name."
elif len(account) > 31:
raise ValueError, "User Names must be shorter than 32 characters."
if account in RESERVED_ACCOUNTS:
raise ValueError, "The account you specified is reserved by the system."
for letter in account:
if letter not in ALLOWED_USER_CHARS:
raise ValueError, "Accounts may only contain ascii letters and " + \
"numbers."
class Accounts:
def __init__(self, users):
self.users = users
def provideAccess(self, user):
authXmlPath = os.path.join(HOST_ROOT, HOSTD_AUTHORIZATION)
try:
dom = minidom.parse(authXmlPath)
# There should only be one ConfigRoot, so we grab the first one.
configRoot = dom.getElementsByTagName("ConfigRoot")[0]
nextAceId = dom.getElementsByTagName("NextAceId")
indentSpace = configRoot._get_firstChild().cloneNode(False)
lastNode = configRoot._get_lastChild()
if not nextAceId:
nextId = 9
for el in dom.getElementsByTagName("ACEData"):
if int(el.getAttribute("id")) > nextId:
nextId = int(el.getAttribute("id"))
nextId = nextId + 1
nextAceIdXML = minidom.parseString(NEXT_AUTH_ID % (nextId + 1)).getElementsByTagName("NextAceId")[0]
configRoot.insertBefore(indentSpace, lastNode)
lastNode = configRoot.insertBefore(nextAceIdXML, lastNode)
else:
nextId = int(nextAceId[0].firstChild.nodeValue)
nextAceId[0].firstChild.replaceWholeText(u"%s" % str(nextId + 1))
lastNode = nextAceId[0]
newUserXML = minidom.parseString(USER_XML_TEMPLATE % {'id' : nextId, 'uname' : user}).getElementsByTagName("ACEData")[0]
configRoot.insertBefore(newUserXML, lastNode)
configRoot.insertBefore(indentSpace, lastNode)
log.debug("Writing new access permissions for '%s' to %s." % (user, authXmlPath))
fp = open(authXmlPath, 'w')
fp.write(configRoot.toxml())
fp.close()
dom.unlink()
except AttributeError, msg:
log.error("Could not add user '%s' to %s: %s" % (user, authXmlPath, msg))
def write(self):
# We have more than just the root user, so we need to add that first.
if self.users:
self.provideAccess("root")
for userDict in self.users:
try:
argv = ["/usr/sbin/useradd", userDict['username']]
execWithRedirect(argv[0], argv, root=HOST_ROOT)
argv = ["/usr/bin/chfn", "-f", userDict['fullName'],
userDict['username']]
execWithRedirect(argv[0], argv, root=HOST_ROOT)
except RuntimeError, msg:
log.error("Error running %s: %s" % (string.join(argv), msg[0]))
if userDict['passwordType'] == userchoices.USERPASSWORD_TYPE_MD5:
useMD5 = True
elif userDict['passwordType'] == \
userchoices.USERPASSWORD_TYPE_CRYPT:
useMD5 = False
else:
raise ValueError, "Don't know how to crypt password."
passwd = Password(userDict['username'], userDict['password'],
useMD5=useMD5)
passwd.write()
self.provideAccess(userDict['username'])
class Password:
def __init__(self, account, password, isCrypted=False, useMD5=True):
self.account = account
if isCrypted:
self.password = password
else:
self.password = cryptPassword(password, useMD5)
def write(self):
try:
argv = [ "/usr/sbin/usermod", "-p", self.password, self.account ]
execWithRedirect(argv[0], argv, root=HOST_ROOT, stderr=None)
except RuntimeError, msg:
log.error("Error running %s: %s" % (string.join(argv), msg[0]))
class RootPassword(Password):
def __init__(self, password, isCrypted=False, useMD5=True):
Password.__init__(self, "root", password, isCrypted, useMD5)
def writeKickstartSection(self, ksFile):
ksFile.write("rootpw --iscrypted %s\n" % (self.password))
def write(self):
Password.write(self)
# remove checking password validation for root
args = ["/usr/bin/chage", "-M", "-1", "root"]
execWithLog(args[0], args, root=HOST_ROOT)
class PasswdFile(migrate.TextConfigFile):
class UserData(migrate.NamedList):
STRUCT = ['name', 'passwd', 'uid', 'gid', 'gecos', 'dir', 'shell']
SEPARATOR = ':'
ELEMENT_TYPE = UserData
def hasName(self, name):
return self.getByName(name) is not None
def getByName(self, name):
for user in self.elements:
if name == user.name:
return user
return None
class GroupFile(migrate.TextConfigFile):
class GroupData(migrate.NamedList):
STRUCT = ['name', 'passwd', 'gid', 'users']
SEPARATOR = ':'
ELEMENT_TYPE = GroupData
def hasName(self, name):
return self.getByName(name) is not None
def getByName(self, name):
for group in self.elements:
if name == group.name:
return group
return None
def hostActionMigrateGroupFile(_context):
# Save the default group file, there is no default gshadow file installed.
newPath = migrate.preserveFile(os.path.join(HOST_ROOT, "etc/group"))
assert newPath != None
migrate.migratePath("/etc/group")
migrate.migratePath("/etc/gshadow")
oldFile = GroupFile.fromFile(os.path.join(HOST_ROOT, "etc/group"))
newFile = GroupFile.fromFile(newPath)
gFile = open(os.path.join(HOST_ROOT, "etc/group"), 'a')
# If there's a shadow file, we need to add any new groups to it.
gshadowFile = None
gshadowFileName = os.path.join(HOST_ROOT, "etc/gshadow")
if os.path.exists(gshadowFileName):
gshadowFile = open(gshadowFileName, 'a')
try:
for groupData in newFile:
if not oldFile.hasName(groupData.name):
gFile.write("%s\n" % ":".join(groupData))
if gshadowFile:
shadowData = [
groupData.name, groupData.passwd, "", groupData.users]
gshadowFile.write("%s\n" % ":".join(shadowData))
finally:
gFile.close()
if gshadowFile:
gshadowFile.close()
def _mergeNewUsers(oldFile, newFile):
for userData in newFile:
if not oldFile.hasName(userData.name):
args = [
"/usr/sbin/useradd",
"-o", # XXX ignore duplicate uids, see bug 278064
"-c", userData.gecos,
"-d", userData.dir,
"-g", userData.gid,
"-s", userData.shell,
"-u", userData.uid,
userData.name
]
try:
execWithLog(args[0], args, root=HOST_ROOT, raiseException=True)
except Exception, e:
raise InstallationError(
"Could not add user, '%s', from old installation." %
userData.name,
e)
def _copyTreeForUser(src, dst, uid, gid):
if not os.path.exists(os.path.dirname(dst)):
os.makedirs(os.path.dirname(dst))
shutil.copytree(src, dst)
os.chown(dst, uid, gid)
for root, _dirs, files in os.walk(dst):
for name in files:
os.chown(os.path.join(root, name), uid, gid)
def _normalizeUsers(oldFile):
'''Performs extra migrations steps for users. Ensures that non-pseudo-users
have a home directory under /home and their shell is sane.'''
for userData in oldFile:
log.debug("migrating user -- %s" % userData.name)
if userData.shell == "/sbin/nologin":
# XXX remove me?
log.debug(" not normalizing pseudo-user")
continue
newHomeDir = os.path.join(HOST_ROOT, userData.dir.lstrip('/'))
oldHomeDir = os.path.join(ESX3_INSTALLATION, userData.dir.lstrip('/'))
if os.path.exists(newHomeDir):
log.debug(" user dir already exists in new install, skipping...")
continue
if not os.path.exists(os.path.join(HOST_ROOT, oldHomeDir.lstrip('/'))):
log.debug(" user dir does not exist in old install, skipping...")
continue
expectedDir = "/home/%s" % userData.name
if userData.dir != expectedDir:
log.warn("changing home directory path for %s to %s" % (
userData.name, expectedDir))
userData.dir = expectedDir
newHomeDir = os.path.join(HOST_ROOT, expectedDir.lstrip('/'))
args = ["/usr/sbin/usermod", "-d", expectedDir, userData.name]
execWithLog(args[0], args, root=HOST_ROOT, raiseException=True)
shellPath = os.path.join(HOST_ROOT, userData.shell.lstrip('/'))
if not os.path.exists(shellPath):
oldShell = userData.shell
userData.shell = "/bin/bash"
log.warn("unknown user shell, %s, switching to %s" % (
oldShell, userData.shell))
args = ["/usr/sbin/usermod", "-s", userData.shell, userData.name]
try:
execWithLog(args[0], args, root=HOST_ROOT, raiseException=True)
except Exception, e:
raise InstallationError("Could not change user shell.", e)
_copyTreeForUser(os.path.join(HOST_ROOT, "etc/skel"),
HOST_ROOT + userData.dir,
int(userData.uid),
int(userData.gid))
os.symlink(oldHomeDir, os.path.join(newHomeDir, "esx3-home"))
def hostActionMigratePasswdFile(_context):
newPath = migrate.preserveFile(os.path.join(HOST_ROOT, "etc/passwd"))
assert newPath != None
migrate.migratePath("/etc/passwd")
migrate.migratePath("/etc/shadow")
oldFile = PasswdFile.fromFile(os.path.join(HOST_ROOT, "etc/passwd"))
newFile = PasswdFile.fromFile(newPath)
_mergeNewUsers(oldFile, newFile)
_normalizeUsers(oldFile)
def cryptPassword(password, useMD5=True):
'''
crypt the password using either simple crypt or md5 algorithms.
Since glibc2, crypt uses a special three character lead to
generate a MD5 string followed by at most 8 characters.
See man crypt(3) for more info.
'''
if useMD5:
salt = "$1$"
saltLen = 8
else:
salt = ""
saltLen = 2
for i in range(saltLen):
salt = salt + random.choice(SALT_CHARS)
return crypt.crypt(password, salt)
class Authentication:
def __init__ (self, options=None):
self.options = options
if not options:
self.options = {}
def writeKickstartSection(self, ksFile):
# XXX - fixme. I'm pretty sure this isn't going to work
# correctly
ksFile.write("authconfig")
ksFile.write(string.join(self.getArgList()))
ksFile.write("\n")
def getArgList(self):
args = []
if 'nis' in self.options and self.options['nis']:
args.append("--enablenis")
if self.options.get('nisDomain', None):
args.append("--nisdomain")
args.append(self.options['nisDomain'])
if self.options.get('nisServer', None):
args.append("--nisserver")
args.append(self.options['nisServer'])
else:
args.append("--disablenis")
if 'ldap' in self.options and self.options['ldap']:
args.append("--enableldap")
else:
args.append("--disableldap")
if 'ldapAuth' in self.options and self.options['ldapAuth']:
args.append("--enableldapauth")
else:
args.append("--disableldapauth")
if ('ldap' in self.options and self.options['ldap']) or \
('ldapAuth' in self.options and self.options['ldapAuth']):
# TODO: make sure that these are required in scripted install or
# we optionally add them
args.append("--ldapserver")
args.append(self.options['ldapServer'])
args.append("--ldapbasedn")
args.append(self.options['ldapBaseDN'])
if 'ldapTLS' in self.options and self.options['ldapTLS']:
args.append("--enableldaptls")
else:
args.append("--disableldaptls")
if 'kerberos' in self.options and self.options['kerberos']:
# TODO: make sure that these are required in scripted install or
# we optionally add them
args.append("--enablekrb5")
args.append("--krb5realm")
args.append(self.options['kerberosRealm'])
args.append("--krb5kdc")
args.append(self.options['kerberosKDC'])
args.append("--krb5adminserver")
args.append(self.options['kerberosServer'])
else:
args.append("--disablekrb5")
return args
def write(self):
args = [ "/usr/sbin/esxcfg-auth", "--kickstart", "--nostart" ]
# Set the required password complexity (see bug 359840)
# XXX Need to keep in sync with visor parameters held here:
# bora/install/vmvisor/environ/etc/pam.d/common-password
args += [ "--usepamqc", "8", "8", "8", "7", "6", "0" ]
args += self.getArgList()
try:
execWithLog(args[0], args, root=HOST_ROOT, raiseException=True)
except Exception, e:
raise InstallationError(
"Could not set authentication method.", e)
def hostActionAuthentication(_context):
workarounds.TouchAuthFiles()
if not userchoices.getAuth():
userchoices.setAuth(nis=False,
kerberos=False,
ldap=False)
choices = userchoices.getAuth()
auth = Authentication(choices)
auth.write()
def hostActionSetupAccounts(context):
rootPassword = userchoices.getRootPassword()
rp = RootPassword(rootPassword['password'],
isCrypted=True,
useMD5=(rootPassword['passwordType'] ==
userchoices.ROOTPASSWORD_TYPE_MD5))
rp.write()
# write out the rest of the users
Accounts(userchoices.getUsers()).write()