novajoin/scripts/novajoin-install

324 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
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
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 user_input(prompt):
while True:
try:
ret = input("%s: " % prompt)
if ret.strip():
return ret.strip()
except EOFError:
raise ConfigurationError('Failed to get user input')
def shell_quote(string):
return "'" + string.replace("'", "'\\''") + "'"
def run(args, stdin=None, raiseonerr=True, env=None,
capture_output=True, skip_output=False, cwd=None,
timeout=None):
"""
Execute a command and return stdin, stdout and the process return code.
:param args: List of arguments for the command
:param stdin: Optional input to the command
:param raiseonerr: If True, raises an exception if the return code is
not zero
:param env: Dictionary of environment variables passed to the command.
When None, current environment is copied
:param capture_output: Capture stderr and stdout
:param skip_output: Redirect the output to /dev/null and do not capture it
:param cwd: Current working directory
:param timeout: Timeout if the command hasn't returned within the specified
number of seconds.
"""
p_in = None
p_out = None
p_err = None
if env is None:
# copy default env
env = copy.deepcopy(os.environ)
env["PATH"] = "/bin:/sbin:/usr/bin:/usr/sbin"
if stdin:
p_in = subprocess.PIPE
if skip_output:
p_out = p_err = open('/dev/null', 'w')
elif capture_output:
p_out = subprocess.PIPE
p_err = subprocess.PIPE
if timeout:
# If a timeout was provided, use the timeout command
# to execute the requested command.
args[0:0] = ['/bin/timeout', str(timeout)]
arg_string = ' '.join(shell_quote(a) for a in args)
logger.debug('Starting external process')
logger.debug('args=%s' % arg_string)
try:
p = subprocess.Popen(args, stdin=p_in, stdout=p_out, stderr=p_err,
close_fds=True, env=env, cwd=cwd)
stdout, stderr = p.communicate(stdin)
stdout, stderr = str(stdout), str(stderr) # Make pylint happy
except KeyboardInterrupt:
logger.debug('Process interrupted')
p.wait()
raise
except:
logger.debug('Process execution failed')
raise
finally:
if skip_output:
p_out.close() # pylint: disable=E1103
if timeout and p.returncode == 124:
logger.debug('Process did not complete before timeout')
logger.debug('Process finished, return code=%s', p.returncode)
# The command and its output may include passwords that we don't want
# to log. Replace those.
if not skip_output:
logger.debug('stdout=%s' % stdout)
logger.debug('stderr=%s' % stderr)
if p.returncode != 0 and raiseonerr:
raise CalledProcessError(p.returncode, arg_string, stdout)
return (stdout, stderr, p.returncode)
def install(args):
logger.info('Installation initiated')
# TODO: verify that machine is IPA client already
logger.info('Installing default config files')
ipaenv = Env()
ipaenv._merge_from_file(IPACONF)
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:
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
)
# FIXME: get the keytab with credentials rather than hardcoding
run(['ipa-getkeytab', '-r', '-s', ipaenv.server,
'-D', 'cn=directory manager', '-w', 'password', '-p',
'admin@%s' % ipaenv.realm, '-k', '/etc/nova/ipauser.keytab'])
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)
# Fixup permissions so only the ipsilon user can read these files
# files.fix_user_dirs(instance_conf, opts['system_user'])
# files.fix_user_dirs(args['data_dir'], opts['system_user'])
# try:
# subprocess.call(['/usr/sbin/restorecon', '-R', args['data_dir']])
# except Exception: # pylint: disable=broad-except
# pass
def parse_args():
parser = argparse.ArgumentParser(description='Nova join Install Options')
parser.add_argument('--version',
action='version', version='%(prog)s 0.1')
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',
help='principal to use to for IPA host management')
parser.add_argument('--password', dest='password',
help='password for the principal')
parser.add_argument('--prompt_password', dest='prompt_password',
action='store_true', default=False,
help='prompt for the principal password')
args = vars(parser.parse_args())
if not args['principal']:
principal = user_input("User authorized to manage hosts")
if not args['password']:
try:
password = getpass.getpass("Password: ")
except EOFError:
password = None
if not 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)