269 lines
8.4 KiB
Python
Executable File
269 lines
8.4 KiB
Python
Executable File
#!/usr/bin/python
|
|
# Copyright 2016 Red Hat, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import argparse
|
|
import logging
|
|
import os
|
|
import pwd
|
|
import shutil
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import copy
|
|
import tempfile
|
|
from subprocess import CalledProcessError
|
|
import getpass
|
|
from string import Template
|
|
from six.moves import input
|
|
from six.moves.configparser import ConfigParser
|
|
from ipalib.config import Env
|
|
from ipapython.ipautil import run, kinit_password, user_input
|
|
|
|
|
|
DATADIR = '/usr/share/novajoin'
|
|
IPACONF = '/etc/ipa/default.conf'
|
|
NOVADIR = '/etc/nova'
|
|
IPACLIENT = 'ipaclient.conf'
|
|
|
|
|
|
class ConfigurationError(StandardError):
|
|
|
|
def __init__(self, message):
|
|
StandardError.__init__(self, message)
|
|
|
|
|
|
LOGFILE = '/var/log/novajoin-install.log'
|
|
logger = logging.getLogger()
|
|
|
|
|
|
def openlogs():
|
|
global logger # pylint: disable=W0603
|
|
if os.path.isfile(LOGFILE):
|
|
try:
|
|
created = '%s' % time.strftime(
|
|
'%Y%m%d%H%M%SZ', time.gmtime(os.path.getctime(LOGFILE)))
|
|
shutil.move(LOGFILE, '%s.%s' % (LOGFILE, created))
|
|
except IOError:
|
|
pass
|
|
logger = logging.getLogger()
|
|
try:
|
|
lh = logging.FileHandler(LOGFILE)
|
|
except IOError as e:
|
|
print >> sys.stderr, 'Unable to open %s (%s)' % (LOGFILE, str(e))
|
|
lh = logging.StreamHandler(sys.stderr)
|
|
formatter = logging.Formatter('[%(asctime)s] %(message)s')
|
|
lh.setFormatter(formatter)
|
|
lh.setLevel(logging.DEBUG)
|
|
logger.addHandler(lh)
|
|
logger.propagate = False
|
|
ch = logging.StreamHandler(sys.stdout)
|
|
formatter = logging.Formatter('%(message)s')
|
|
ch.setFormatter(formatter)
|
|
ch.setLevel(logging.INFO)
|
|
logger.addHandler(ch)
|
|
|
|
|
|
def write_from_template(destfile, template, opts):
|
|
with open(template) as f:
|
|
t = Template(f.read())
|
|
text = t.substitute(**opts)
|
|
with open(destfile, 'w+') as f:
|
|
f.write(text)
|
|
logger.debug(destfile)
|
|
logger.debug(text)
|
|
|
|
|
|
def install(args):
|
|
logger.info('Installation initiated')
|
|
|
|
ipaenv = Env()
|
|
ipaenv._merge_from_file(IPACONF)
|
|
|
|
if not os.path.exists('/etc/ipa/default.conf'):
|
|
raise ConfigurationError('Must be enrolled in IPA')
|
|
|
|
try:
|
|
if os.path.exists('/etc/nova/ipauser.keytab'):
|
|
os.unlink('/etc/nova/ipauser.keytab')
|
|
except OSError as e:
|
|
raise ConfigurationError(
|
|
'Could not remove /etc/nova/ipauser.keytab: %s' % e
|
|
)
|
|
|
|
ccache_dir = tempfile.mkdtemp(prefix='krbcc')
|
|
|
|
ccache_name = os.path.join(ccache_dir, 'ccache')
|
|
env = {"PATH":
|
|
"/bin:/sbin:/usr/kerberos/bin:/usr/kerberos/sbin:/usr/bin:/usr/sbin"
|
|
}
|
|
env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] = ccache_name
|
|
|
|
principal = args['principal']
|
|
if principal.find('@') == -1:
|
|
principal = '%s@%s' % (principal, ipaenv.realm)
|
|
|
|
try:
|
|
kinit_password(principal, args['password'], ccache_name)
|
|
except RuntimeError as e:
|
|
raise ConfigurationError("Kerberos authentication failed: %s" % e)
|
|
|
|
run(['ipa-getkeytab',
|
|
'-s', ipaenv.server,
|
|
'-p', 'nova/%s@%s' % (args['hostname'], ipaenv.realm),
|
|
'-k', '/etc/nova/ipauser.keytab'],
|
|
env=env)
|
|
|
|
logger.info('Installing default config files')
|
|
|
|
confopts = {'FQDN': args['hostname'],
|
|
'MASTER': ipaenv.server} # pylint: disable=no-member
|
|
|
|
nova_ipa_conf = os.path.join(NOVADIR, IPACLIENT)
|
|
|
|
write_from_template(nova_ipa_conf,
|
|
os.path.join(DATADIR, IPACLIENT + '.template'),
|
|
confopts)
|
|
|
|
FILES = ['setup-ipa-client.sh', 'cloud-config.json']
|
|
for fn in FILES:
|
|
dst = os.path.join(NOVADIR, fn)
|
|
source = os.path.join(DATADIR, fn)
|
|
logger.info('Installing %s' % dst)
|
|
shutil.copyfile(source, dst)
|
|
|
|
config = ConfigParser()
|
|
config.read('/etc/nova/nova.conf')
|
|
config.set('DEFAULT',
|
|
'vendordata_jsonfile_path',
|
|
'/etc/nova/cloud-config.json')
|
|
|
|
# set the default domain to the IPA domain. This is added to the
|
|
# instance name to set the hostname.
|
|
config.set('DEFAULT',
|
|
'dhcp_domain',
|
|
ipaenv.domain)
|
|
|
|
with open('/etc/nova/nova.conf', 'w') as f:
|
|
config.write(f)
|
|
|
|
try:
|
|
user = pwd.getpwnam(args['user'])
|
|
except KeyError:
|
|
raise ConfigurationError('User: %s not found on the system' %
|
|
args['user'])
|
|
|
|
os.chown('/etc/nova/ipauser.keytab', user.pw_uid, user.pw_gid)
|
|
os.chmod('/etc/nova/ipauser.keytab', 0o600)
|
|
|
|
logger.info('Importing IPA metadata')
|
|
(stdout, stderr, returncode) = run(
|
|
['glance',
|
|
'md-namespace-import',
|
|
'--file',
|
|
'/usr/share/freeipa.json'], raiseonerr=False)
|
|
if returncode != 0:
|
|
logger.error('Adding IPA metadata failed: %s' % stderr)
|
|
|
|
logger.info('Creating IPA permissions')
|
|
(stdout, stderr, returncode) = run(
|
|
['/usr/libexec/novajoin-ipa-setup.sh'], raiseonerr=False)
|
|
if returncode != 0:
|
|
logger.error('Creating IPA permissions failed')
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(description='Nova join Install Options')
|
|
parser.add_argument('--hostname',
|
|
help='Machine\'s fully qualified host name')
|
|
parser.add_argument('--user',
|
|
help='User that nova services run as',
|
|
default='nova')
|
|
parser.add_argument('--principal', dest='principal', default='admin',
|
|
help='principal to use to setup IPA integration')
|
|
parser.add_argument('--password', dest='password',
|
|
help='password for the principal')
|
|
parser.add_argument('--password-file', dest='passwordfile',
|
|
help='path to file containing password for '
|
|
'the principal')
|
|
|
|
args = vars(parser.parse_args())
|
|
|
|
if not args['principal']:
|
|
args['principal'] = user_input("IPA admin user", "admin",
|
|
allow_empty=False)
|
|
|
|
if args['passwordfile']:
|
|
try:
|
|
with open(args['passwordfile']) as f:
|
|
args['password'] = f.read()
|
|
except IOError as e:
|
|
raise ConfigurationError('Unable to read password file: %s'
|
|
% e)
|
|
if not args['password']:
|
|
try:
|
|
args['password'] = getpass.getpass("Password for %s: " %
|
|
args['principal'])
|
|
except EOFError:
|
|
password = None
|
|
if not args['password']:
|
|
raise ConfigurationError('Password must be provided.')
|
|
|
|
if not args['hostname']:
|
|
args['hostname'] = socket.getfqdn()
|
|
|
|
if len(args['hostname'].split('.')) < 2:
|
|
raise ConfigurationError('Hostname: %s is not a FQDN' %
|
|
args['hostname'])
|
|
|
|
try:
|
|
pwd.getpwnam(args['user'])
|
|
except KeyError:
|
|
raise ConfigurationError('User: %s not found on the system' %
|
|
args['user'])
|
|
|
|
return args
|
|
|
|
if __name__ == '__main__':
|
|
opts = []
|
|
out = 0
|
|
openlogs()
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
try:
|
|
opts = parse_args()
|
|
|
|
logger.debug('Installation arguments:')
|
|
for k in sorted(opts.iterkeys()):
|
|
logger.debug('%s: %s', k, opts[k])
|
|
|
|
install(opts)
|
|
except Exception as e: # pylint: disable=broad-except
|
|
logger.info(str(e)) # emit message to console
|
|
logger.debug(e, exc_info=1) # add backtrace information to logfile
|
|
|
|
logger.info('Installation aborted.')
|
|
logger.info('See log file %s for details' % LOGFILE)
|
|
out = 1
|
|
except SystemExit:
|
|
out = 1
|
|
raise
|
|
finally:
|
|
if out == 0:
|
|
logger.info('Installation complete.')
|
|
logger.info(
|
|
'Please restart nova-compute to enable the join service.')
|
|
sys.exit(out)
|