315 lines
10 KiB
Python
Executable File
315 lines
10 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 ipapython.ipautil import run, kinit_password, user_input
|
|
from ipalib import api
|
|
from ipalib import errors
|
|
|
|
|
|
DATADIR = '/usr/share/novajoin'
|
|
NOVADIR = '/etc/nova'
|
|
IPACONF = '/etc/ipa/default.conf'
|
|
JOINCONF = '/etc/join/join.conf'
|
|
KEYTAB = '/etc/join/krb5.keytab'
|
|
|
|
|
|
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')
|
|
|
|
if not os.path.exists('/etc/ipa/default.conf'):
|
|
raise ConfigurationError('Must be enrolled in IPA')
|
|
|
|
api.bootstrap(context='novajoin')
|
|
api.finalize()
|
|
|
|
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, api.env.realm)
|
|
|
|
try:
|
|
kinit_password(principal, args['password'], ccache_name)
|
|
except RuntimeError as e:
|
|
raise ConfigurationError("Kerberos authentication failed: %s" % e)
|
|
|
|
api.Backend.rpcclient.connect()
|
|
|
|
try:
|
|
result = api.Command.service_add(u'nova/%s@%s' %
|
|
(args['hostname'], api.env.realm),
|
|
force=True)
|
|
except errors.DuplicateEntry:
|
|
pass
|
|
except Exception as e:
|
|
raise ConfigurationError(
|
|
'Failed to add service: %s' % e)
|
|
|
|
try:
|
|
if os.path.exists(KEYTAB):
|
|
os.unlink(KEYTAB)
|
|
except OSError as e:
|
|
raise ConfigurationError(
|
|
'Could not remove %s: %s' % (KEYTAB, e)
|
|
)
|
|
|
|
run(['ipa-getkeytab',
|
|
'-s', api.env.server,
|
|
'-p', 'nova/%s@%s' % (args['hostname'], api.env.realm),
|
|
'-k', KEYTAB],
|
|
env=env)
|
|
|
|
logger.info('Installing default config files')
|
|
|
|
confopts = {'FQDN': args['hostname'],
|
|
'MASTER': api.env.server, # pylint: disable=no-member
|
|
'DOMAIN': api.env.domain, # pylint: disable=no-member
|
|
'KEYSTONE_AUTH_URI': args['keystone_auth_uri'],
|
|
'KEYSTONE_AUTH_URL': args['keystone_auth_url'],
|
|
'KEYSTONE_IDENTITY': args['keystone_identity'],
|
|
'NOVA_PASSWORD': args['nova_password'],
|
|
}
|
|
|
|
write_from_template(JOINCONF,
|
|
os.path.join(DATADIR, 'join.conf.template'),
|
|
confopts)
|
|
|
|
FILES = ['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',
|
|
api.env.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(KEYTAB, user.pw_uid, user.pw_gid)
|
|
os.chmod(KEYTAB, 0o600)
|
|
|
|
logger.info('Importing IPA metadata')
|
|
(stdout, stderr, returncode) = run(
|
|
['glance',
|
|
'md-namespace-import',
|
|
'--file',
|
|
'/usr/share/novajoin/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')
|
|
parser.add_argument('--keystone-auth-uri', dest='keystone_auth_uri',
|
|
help='Keystone auth URI')
|
|
parser.add_argument('--keystone-auth-url', dest='keystone_auth_url',
|
|
help='Keystone auth URL')
|
|
parser.add_argument('--keystone-identity', dest='keystone_identity',
|
|
help='Keystone identity URI')
|
|
parser.add_argument('--nova-password', dest='nova_password',
|
|
help='Nova service user password')
|
|
|
|
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:
|
|
args['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'])
|
|
|
|
if not args['keystone_auth_uri']:
|
|
args['keystone_auth_uri'] = user_input("Keysone auth URI", "",
|
|
allow_empty=False)
|
|
|
|
if not args['keystone_auth_url']:
|
|
args['keystone_auth_url'] = user_input("Keysone auth URL", "",
|
|
allow_empty=False)
|
|
|
|
if not args['keystone_identity']:
|
|
args['keystone_identity'] = user_input("Keysone identity URI", "",
|
|
allow_empty=False)
|
|
|
|
if not args['nova_password']:
|
|
try:
|
|
args['nova_password'] = getpass.getpass("nova service Password: ")
|
|
except EOFError:
|
|
args['nova_password'] = None
|
|
if not args['nova_password']:
|
|
raise ConfigurationError('nova service user password required.')
|
|
|
|
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-api to enable the join service.')
|
|
sys.exit(out)
|