Pythonize instack-install-undercloud

-Very incomplete testing right now
-Puts the password and stackrc files in the current user's home
 directory during the install.  Given that they now have secure
 permissions and we recommend doing that anyway, I think it's fine,
 but it is a non-trivial change in behavior.

 This was done because it's awkward to read a root-owned file from
 a Python process running as a regular user.
-Uses oslo.config instead of the bash-style answers file. A sample
 conf file created by the oslo.config generator is included (for
 now, although we may want to generate that dynamically at some
 point).  Backwards compatibility with existing answers files is
 maintained for now, but is deprecated.
-Hard-codes the image path in instack-test-overcloud to .  It's
 difficult to extract the value from the conf file in bash (unless
 they overrode the default, there's nothing for ConfigParser to read),
 and since it's just a simple sanity test script I think that's okay,
 at least for now.

Change-Id: I09270997dea7fdad2b40dfb303967ff425b55a9b
This commit is contained in:
Ben Nemec 2015-04-30 21:48:25 +00:00 committed by James Slagle
parent bf9e96920d
commit a82f4dc363
20 changed files with 818 additions and 356 deletions

1
.gitignore vendored
View File

@ -23,6 +23,7 @@ pip-log.txt
# Unit test / coverage reports
.coverage
.tox
.testrepository
nosetests.xml
# Translations

4
.testr.conf Normal file
View File

@ -0,0 +1,4 @@
[DEFAULT]
test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./instack_undercloud ./instack_undercloud $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

View File

@ -0,0 +1,3 @@
[DEFAULT]
output_file = undercloud.conf.sample
namespace = instack-undercloud

View File

@ -28,7 +28,6 @@ Installing the Undercloud
sudo hostnamectl set-hostname myhost.mydomain
sudo hostnamectl set-hostname --transient myhost.mydomain
export HOSTNAME=myhost.mydomain
An entry for the system's FQDN hostname is also needed in /etc/hosts. For
example, if the system is named *myhost.mydomain*, /etc/hosts should have
@ -72,9 +71,9 @@ Installing the Undercloud
.. admonition:: Baremetal
:class: baremetal
Copy in the sample answers file and edit it to reflect your environment::
Copy in the sample configuration file and edit it to reflect your environment::
cp /usr/share/instack-undercloud/instack.answers.sample ~/instack.answers
cp /usr/share/instack-undercloud/undercloud.conf.sample ~/undercloud.conf
Install the undercloud::
@ -82,12 +81,13 @@ Installing the Undercloud
openstack undercloud install
Once the install script has run to completion, you should take note of the
files ``/root/stackrc`` and ``/root/tripleo-undercloud-passwords``. Both of
these files will be needed to interact with the installed undercloud. Copy them
to the home directory for easier use later::
Once the install has completed, you should take note of the files ``stackrc`` and
``undercloud-passwords.conf``. You can source ``stackrc`` to interact with the
undercloud via the OpenStack command-line client. ``undercloud-passwords.conf``
contains the passwords used for each service in the undercloud. These passwords
will be automatically reused if the undercloud is reinstalled on the same system,
so it is not necessary to copy them to ``undercloud.conf``.
sudo cp /root/tripleo-undercloud-passwords .
sudo chown $USER: tripleo-undercloud-passwords
sudo cp /root/stackrc .
sudo chown $USER: stackrc
.. note::
Any passwords set in ``undercloud.conf`` will take precedence over the ones in
``undercloud-passwords.conf``.

View File

@ -17,7 +17,7 @@ visudo -c
mkdir -p /home/stack/.ssh
cp /tmp/in_target.d/id_rsa_virt_power /home/stack/.ssh/id_rsa_virt_power
cp /tmp/in_target.d/id_rsa_virt_power.pub /home/stack/.ssh/id_rsa_virt_power.pub
cp /tmp/in_target.d/instack.answers.sample /home/stack/instack.answers
cp /tmp/in_target.d/undercloud.conf.sample /home/stack/undercloud.conf
cp /tmp/in_target.d/$TE_DATAFILE /home/stack/instackenv.json
chown -R stack:stack /home/stack

View File

@ -1,161 +0,0 @@
# instack answers file
### DEPLOYMENT_MODE ###
# Deployment mode to setup on this Undercloud.
# Choices are poc and scale:
# poc will allow deployment of a single role to heterogenous hardware
# scale will allow deployment of a single role only to homogenous hardware.
DEPLOYMENT_MODE=poc
### IMAGE_PATH ###
# Local file path to the downloaded images.
# The path should be a directory readable by the current user that contains
# the full set of downloaded images.
IMAGE_PATH=.
### LOCAL_IP ###
# IP address to assign to the interface on the Undercloud that will
# be handling the PXE boots and DHCP for Overcloud instances.
# LOCAL_IP will be assigned to LOCAL_INTERFACE, and must be in
# IP/PREFIX format.
LOCAL_IP=192.0.2.1/24
### LOCAL_INTERFACE ###
# Network interface on the Undercloud that will be handling the PXE boots and
# DHCP for Overcloud instances.
# LOCAL_INTERFACE will be assigned the address from LOCAL_IP
LOCAL_INTERFACE=eth1
### MASQUERADE_NETWORK ###
# Network that will be masqueraded for external access if required.
MASQUERADE_NETWORK=192.0.2.0/24
### DHCP_START ###
# Start of DHCP Allocation range for PXE and DHCP of Overcloud instances
DHCP_START=192.0.2.5
### DHCP_END ###
# End of DHCP Allocation range for PXE and DHCP of Overcloud instances
DHCP_END=192.0.2.24
### NETWORK_CIDR ###
# Network cidr for neutron managed network for Overcloud instances
NETWORK_CIDR=192.0.2.0/24
### NETWORK_GATEWAY ###
# Network gateway for neturon managed network for Overcloud instances
NETWORK_GATEWAY=192.0.2.1
### DISCOVERY_INTERFACE ###
# Network interface on which discovery dnsmasq will listen
# If in doubt, do not change
DISCOVERY_INTERFACE=br-ctlplane
### DISCOVERY_IPRANGE ###
# Temporary IP range that will be given to nodes during discovery process
# Should not overlap with Neutron range, but should be in the same network
DISCOVERY_IPRANGE=192.0.2.100,192.0.2.120
### DISCOVERY_RUNBENCH ###
# Determines whether benchmarks are run when discovering nodes
# Set to 1 to enable
DISCOVERY_RUNBENCH=0
### UNDERCLOUD_DEBUG ###
# Whether to enable the debug log level for OpenStack services
UNDERCLOUD_DEBUG=true
### Database password ###
# Password used for MySQL databases
# If left unset, one will be automatically generated
UNDERCLOUD_DB_PASSWORD=
### Admin Token ###
# Keystone admin token
# If left unset, one will be automatically generated
UNDERCLOUD_ADMIN_TOKEN=
### Admin password ###
# Keystone admin password
# If left unset, one will be automatically generated
UNDERCLOUD_ADMIN_PASSWORD=
### Glance password ###
# Glance service password
# If left unset, one will be automatically generated
UNDERCLOUD_GLANCE_PASSWORD=
### Heat password ###
# Heat service password
# If left unset, one will be automatically generated
UNDERCLOUD_HEAT_PASSWORD=
### Neutron password ###
# Neutron service password
# If left unset, one will be automatically generated
UNDERCLOUD_NEUTRON_PASSWORD=
### Nova password ###
# Nova service password
# If left unset, one will be automatically generated
UNDERCLOUD_NOVA_PASSWORD=
### Ironic password ###
# Ironic service password
# If left unset, one will be automatically generated
UNDERCLOUD_IRONIC_PASSWORD=
### Tuskar password ###
# Tuskar service password
# If left unset, one will be automatically generated
UNDERCLOUD_TUSKAR_PASSWORD=
### Ceilometer password ###
# Ceilometer service password
# If left unset, one will be automatically generated
UNDERCLOUD_CEILOMETER_PASSWORD=
### Ceilometer metering secret ###
# Ceilometer metering secret
# If left unset, one will be automatically generated
UNDERCLOUD_CEILOMETER_METERING_SECRET=
### Ceilometer snmpd user ###
# Ceilometer snmpd user
# If left unset, one will be automatically generated
UNDERCLOUD_CEILOMETER_SNMPD_USER=
### Ceilometer snmpd password ###
# Ceilometer snmpd password
# If left unset, one will be automatically generated
UNDERCLOUD_CEILOMETER_SNMPD_PASSWORD=
### Swift password ###
# Swift password
# If left unset, one will be automatically generated
UNDERCLOUD_SWIFT_PASSWORD=
### Rabbit Cookie ###
# Rabbit Cookie
# If left unset, one will be automatically generated
UNDERCLOUD_RABBIT_COOKIE=
### Rabbit Password ###
# Rabbit Password
# If left unset, one will be automatically generated
UNDERCLOUD_RABBIT_PASSWORD=
### Rabbit Username ###
# Rabbit Username
# If left unset, one will be automatically generated
UNDERCLOUD_RABBIT_USERNAME=
### Heat Stack Domain Admin Password ###
# Heat Stack Domain Admin Password
# If left unset, one will be automatically generated
UNDERCLOUD_HEAT_STACK_DOMAIN_ADMIN_PASSWORD=
### Swift Hash Suffix ###
# Swift Hash Suffix
# If left unset, one will be automatically generated
UNDERCLOUD_SWIFT_HASH_SUFFIX=

View File

View File

View File

@ -0,0 +1,98 @@
# Copyright 2015 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 io
import fixtures
import mock
from oslotest import base
from instack_undercloud import undercloud
# TODO(bnemec): Test all the things
# TODO(bnemec): Stop the code from logging to the real log file during tests
class TestUndercloud(base.BaseTestCase):
@mock.patch('instack_undercloud.undercloud._check_hostname')
@mock.patch('instack_undercloud.undercloud._run_command')
@mock.patch('instack_undercloud.undercloud._configure_ssh_keys')
@mock.patch('instack_undercloud.undercloud._run_orc')
@mock.patch('instack_undercloud.undercloud._run_instack')
@mock.patch('instack_undercloud.undercloud._generate_environment')
@mock.patch('instack_undercloud.undercloud._load_config')
def test_install(self, mock_load_config, mock_generate_environment,
mock_run_instack, mock_run_orc, mock_configure_ssh_keys,
mock_run_command, mock_check_hostname):
fake_env = mock.MagicMock()
mock_generate_environment.return_value = fake_env
undercloud.install('.')
mock_generate_environment.assert_called_with('.')
mock_run_instack.assert_called_with(fake_env)
mock_run_orc.assert_called_with(fake_env)
mock_run_command.assert_called_with(
['sudo', 'rm', '-f', '/tmp/svc-map-services'], None, 'rm')
class TestCheckHostname(base.BaseTestCase):
@mock.patch('instack_undercloud.undercloud._run_command')
def test_correct(self, mock_run_command):
mock_run_command.side_effect = ['test-hostname', 'test-hostname']
self.useFixture(fixtures.EnvironmentVariable('HOSTNAME',
'test-hostname'))
fake_hosts = io.StringIO(u'127.0.0.1 test-hostname\n')
with mock.patch('instack_undercloud.undercloud.open',
return_value=fake_hosts, create=True):
undercloud._check_hostname()
@mock.patch('instack_undercloud.undercloud._run_command')
def test_static_transient_mismatch(self, mock_run_command):
mock_run_command.side_effect = ['test-hostname', 'other-hostname']
self.useFixture(fixtures.EnvironmentVariable('HOSTNAME',
'test-hostname'))
fake_hosts = io.StringIO(u'127.0.0.1 test-hostname\n')
with mock.patch('instack_undercloud.undercloud.open',
return_value=fake_hosts, create=True):
self.assertRaises(RuntimeError, undercloud._check_hostname)
@mock.patch('instack_undercloud.undercloud._run_command')
def test_missing_entry(self, mock_run_command):
mock_run_command.side_effect = ['test-hostname', 'test-hostname']
self.useFixture(fixtures.EnvironmentVariable('HOSTNAME',
'test-hostname'))
fake_hosts = io.StringIO(u'127.0.0.1 other-hostname\n')
with mock.patch('instack_undercloud.undercloud.open',
return_value=fake_hosts, create=True):
self.assertRaises(RuntimeError, undercloud._check_hostname)
@mock.patch('instack_undercloud.undercloud._run_command')
def test_no_substring_match(self, mock_run_command):
mock_run_command.side_effect = ['test-hostname', 'test-hostname']
self.useFixture(fixtures.EnvironmentVariable('HOSTNAME',
'test-hostname'))
fake_hosts = io.StringIO(u'127.0.0.1 test-hostname-bad\n')
with mock.patch('instack_undercloud.undercloud.open',
return_value=fake_hosts, create=True):
self.assertRaises(RuntimeError, undercloud._check_hostname)
@mock.patch('instack_undercloud.undercloud._run_command')
def test_commented(self, mock_run_command):
mock_run_command.side_effect = ['test-hostname', 'test-hostname']
self.useFixture(fixtures.EnvironmentVariable('HOSTNAME',
'test-hostname'))
fake_hosts = io.StringIO(u""" #127.0.0.1 test-hostname\n
127.0.0.1 other-hostname\n""")
with mock.patch('instack_undercloud.undercloud.open',
return_value=fake_hosts, create=True):
self.assertRaises(RuntimeError, undercloud._check_hostname)

View File

@ -0,0 +1,511 @@
# Copyright 2015 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 ConfigParser
import copy
import errno
import getpass
import hashlib
import logging
import os
import platform
import socket
import StringIO
import subprocess
import uuid
from novaclient import client as novaclient
from novaclient import exceptions
from oslo_config import cfg
import six
CONF_PATH = os.path.expanduser('~/undercloud.conf')
# NOTE(bnemec): Deprecated
ANSWERS_PATH = os.path.expanduser('~/instack.answers')
PASSWORD_PATH = os.path.expanduser('~/undercloud-passwords.conf')
LOG_FILE = os.path.expanduser('~/.instack/install-undercloud.log')
DEFAULT_LOG_LEVEL = logging.DEBUG
DEFAULT_LOG_FORMAT = '%(asctime)s %(levelname)s: %(message)s'
try:
os.makedirs(os.path.dirname(LOG_FILE))
except OSError as e:
if e.errno != errno.EEXIST:
raise
logging.basicConfig(filename=LOG_FILE,
format=DEFAULT_LOG_FORMAT,
level=DEFAULT_LOG_LEVEL)
LOG = logging.getLogger(__name__)
LOG.addHandler(logging.StreamHandler())
CONF = cfg.CONF
COMPLETION_MESSAGE = """
#############################################################################
instack-install-undercloud complete.
The file containing this installation's passwords is at
%(password_path)s.
There is also a stackrc file at %(stackrc_path)s.
These files are needed to interact with the OpenStack services, and should be
secured.
#############################################################################
"""
# When adding new options to the lists below, make sure to regenerate the
# sample config by running "tox -e genconfig" in the project root.
_opts = [
cfg.StrOpt('deployment_mode',
default='poc',
choices=['poc', 'scale'],
help=('Deployment mode for this undercloud. "poc" will allow '
'deployment of a single role to heterogenous hardware. '
'"scale" will allow deployment of a single role only to '
'homogenous hardware.'
)
),
cfg.StrOpt('image_path',
default='.',
help=('Local file path to the necessary images. The path '
'should be a directory readable by the current user '
'that contains the full set of images.'),
),
cfg.StrOpt('local_ip',
default='192.0.2.1/24',
help=('IP information for the interface on the Undercloud '
'that will be handling the PXE boots and DHCP for '
'Overcloud instances. The IP portion of the value will '
'be assigned to the network interface defined by '
'local_interface, with the netmask defined by the '
'prefix portion of the value.')
),
cfg.StrOpt('local_interface',
default='eth1',
help=('Network interface on the Undercloud that will be '
'handling the PXE boots and DHCP for Overcloud '
'instances.')
),
cfg.StrOpt('masquerade_network',
default='192.0.2.0/24',
help=('Network that will be masqueraded for external access, '
'if required.')
),
cfg.StrOpt('dhcp_start',
default='192.0.2.5',
help=('Start of DHCP allocation range for PXE and DHCP of '
'Overcloud instances.')
),
cfg.StrOpt('dhcp_end',
default='192.0.2.24',
help=('End of DHCP allocation range for PXE and DHCP of '
'Overcloud instances.')
),
cfg.StrOpt('network_cidr',
default='192.0.2.0/24',
help=('Network CIDR for the Neutron-managed network for '
'Overcloud instances.')
),
cfg.StrOpt('network_gateway',
default='192.0.2.1',
help=('Network gateway for the Neutron-managed network for '
'Overcloud instances.')
),
cfg.StrOpt('discovery_interface',
default='br-ctlplane',
help=('Network interface on which discovery dnsmasq will '
'listen. If in doubt, use the default value.')
),
cfg.StrOpt('discovery_iprange',
default='192.0.2.100,192.0.2.120',
help=('Temporary IP range that will be given to nodes during '
'the discovery process. Should not overlap with the '
'range defined by dhcp_start and dhcp_end, but should '
'be in the same network.')
),
cfg.BoolOpt('discovery_runbench',
default=False,
help='Whether to run benchmarks when discovering nodes.'
),
cfg.BoolOpt('undercloud_debug',
default=True,
help=('Whether to enable the debug log level for Undercloud '
'OpenStack services.')
),
]
# Passwords, tokens, hashes
_auth_opts = [
cfg.StrOpt('undercloud_db_password',
help=('Password used for MySQL databases. '
'If left unset, one will be automatically generated.')
),
cfg.StrOpt('undercloud_admin_token',
help=('Keystone admin token. '
'If left unset, one will be automatically generated.')
),
cfg.StrOpt('undercloud_admin_password',
help=('Keystone admin password. '
'If left unset, one will be automatically generated.')
),
cfg.StrOpt('undercloud_glance_password',
help=('Glance service password. '
'If left unset, one will be automatically generated.')
),
cfg.StrOpt('undercloud_heat_password',
help=('Heat service password. '
'If left unset, one will be automatically generated.')
),
cfg.StrOpt('undercloud_neutron_password',
help=('Neutron service password. '
'If left unset, one will be automatically generated.')
),
cfg.StrOpt('undercloud_nova_password',
help=('Nova service password. '
'If left unset, one will be automatically generated.')
),
cfg.StrOpt('undercloud_ironic_password',
help=('Ironic service password. '
'If left unset, one will be automatically generated.')
),
cfg.StrOpt('undercloud_tuskar_password',
help=('Tuskar service password. '
'If left unset, one will be automatically generated.')
),
cfg.StrOpt('undercloud_ceilometer_password',
help=('Ceilometer service password. '
'If left unset, one will be automatically generated.')
),
cfg.StrOpt('undercloud_ceilometer_metering_secret',
help=('Ceilometer metering secret. '
'If left unset, one will be automatically generated.')
),
cfg.StrOpt('undercloud_ceilometer_snmpd_user',
help=('Ceilometer snmpd user. '
'If left unset, one will be automatically generated.')
),
cfg.StrOpt('undercloud_ceilometer_snmpd_password',
help=('Ceilometer snmpd password. '
'If left unset, one will be automatically generated.')
),
cfg.StrOpt('undercloud_swift_password',
help=('Swift service password. '
'If left unset, one will be automatically generated.')
),
cfg.StrOpt('undercloud_rabbit_cookie',
help=('Rabbitmq cookie. '
'If left unset, one will be automatically generated.')
),
cfg.StrOpt('undercloud_rabbit_password',
default='guest',
help='Rabbitmq password.'
),
cfg.StrOpt('undercloud_rabbit_username',
default='guest',
help='Rabbitmq username.'
),
cfg.StrOpt('undercloud_heat_stack_domain_admin_password',
help=('Heat stack domain admin password. '
'If left unset, one will be automatically generated.')
),
cfg.StrOpt('undercloud_swift_hash_suffix',
help=('Swift hash suffix. '
'If left unset, one will be automatically generated.')
),
]
CONF.register_opts(_opts)
CONF.register_opts(_auth_opts, group='auth')
def list_opts():
return [(None, copy.deepcopy(_opts)),
('auth', copy.deepcopy(_auth_opts)),
]
def _load_config():
conf_params = []
if os.path.isfile(PASSWORD_PATH):
conf_params += ['--config-file', PASSWORD_PATH]
if os.path.isfile(CONF_PATH):
conf_params += ['--config-file', CONF_PATH]
CONF(conf_params)
def _run_command(args, env=None, name=None):
"""Run the command defined by args and return its output
:param args: List of arguments for the command to be run.
:param env: Dict defining the environment variables. Pass None to use
the current environment.
:param name: User-friendly name for the command being run. A value of
None will cause args[0] to be used.
"""
if name is None:
name = args[0]
try:
return subprocess.check_output(args,
stderr=subprocess.STDOUT,
env=env)
except subprocess.CalledProcessError as e:
LOG.error('%s failed: %s', name, e.output)
raise
def _run_live_command(args, env=None, name=None):
"""Run the command defined by args and log its output
Takes the same arguments as _run_command, but runs the process
asynchronously so the output can be logged while the process is still
running.
"""
process = subprocess.Popen(args, env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
while True:
line = process.stdout.readline()
if line:
LOG.info(line.rstrip())
if line == '' and process.poll() is not None:
break
if process.returncode != 0:
raise RuntimeError('%s failed. See log for details.', name)
def _check_hostname():
"""Check system hostname configuration
Rabbit requires a pretty specific hostname configuration. This attempts
to verify the configuration is correct before continuing with
installation.
"""
LOG.info('Checking for a FQDN hostname...')
args = ['sudo', 'hostnamectl', '--static']
detected_static_hostname = _run_command(args, name='hostnamectl').rstrip()
LOG.info('Static hostname detected as %s', detected_static_hostname)
args = ['sudo', 'hostnamectl', '--transient']
detected_transient_hostname = _run_command(args,
name='hostnamectl').rstrip()
LOG.info('Transient hostname detected as %s', detected_transient_hostname)
if detected_static_hostname != detected_transient_hostname:
LOG.error('Static hostname "%s" does not match transient hostname '
'"%s".', detected_static_hostname,
detected_transient_hostname)
LOG.error('Use hostnamectl to set matching hostnames.')
raise RuntimeError('Static and transient hostnames do not match')
with open('/etc/hosts') as hosts_file:
for line in hosts_file:
if (not line.lstrip().startswith('#') and
detected_static_hostname in line.split()):
break
else:
LOG.error('Static hostname not set in /etc/hosts.')
LOG.error('Please add a line to /etc/hosts for the static '
'hostname.')
raise RuntimeError('Static hostname not set in /etc/hosts')
def _generate_password():
"""Create a random password
Copied from rdomanager-oscplugin. This should eventually live in
tripleo-common.
"""
uuid_str = six.text_type(uuid.uuid1()).encode("UTF-8")
return hashlib.sha1(uuid_str).hexdigest()
def _generate_environment(instack_root):
"""Generate an environment dict for instack
The returned dict will have the necessary values for use as the env
parameter when calling instack via the subprocess module.
:param instack_root: The path containing the instack-undercloud elements
and json files.
"""
instack_env = os.environ
# Rabbit uses HOSTNAME, so we need to make sure it's right
instack_env['HOSTNAME'] = socket.gethostname()
# Find the paths we need
json_file_dir = '/usr/share/instack-undercloud/json-files'
if not os.path.isdir(json_file_dir):
json_file_dir = os.path.join(instack_root, 'json-files')
instack_undercloud_elements = '/usr/share/instack-undercloud'
if not os.path.isdir(instack_undercloud_elements):
instack_undercloud_elements = os.path.join(instack_root, 'elements')
tripleo_puppet_elements = '/usr/share/tripleo-puppet-elements'
if not os.path.isdir(tripleo_puppet_elements):
tripleo_puppet_elements = os.path.join(os.getcwd(),
'tripleo-puppet-elements',
'elements')
if 'ELEMENTS_PATH' in os.environ:
instack_env['ELEMENTS_PATH'] = os.environ['ELEMENTS_PATH']
else:
instack_env['ELEMENTS_PATH'] = (
'%s:%s:'
'/usr/share/tripleo-image-elements:'
'/usr/share/diskimage-builder/elements'
) % (tripleo_puppet_elements, instack_undercloud_elements)
# Distro-specific values
distro = platform.linux_distribution()[0]
if distro.startswith('Red Hat Enterprise Linux'):
instack_env['NODE_DIST'] = os.environ.get('NODE_DIST') or 'rhel7'
instack_env['JSONFILE'] = (
os.environ.get('JSONFILE') or
os.path.join(json_file_dir, 'rhel-7-undercloud-packages.json')
)
instack_env['REG_METHOD'] = 'disable'
instack_env['REG_HALT_UNREGISTER'] = '1'
elif distro.startswith('CentOS'):
instack_env['NODE_DIST'] = os.environ.get('NODE_DIST') or 'centos7'
instack_env['JSONFILE'] = (
os.environ.get('JSONFILE') or
os.path.join(json_file_dir, 'centos-7-undercloud-packages.json')
)
elif distro.startswith('Fedora'):
instack_env['NODE_DIST'] = os.environ.get('NODE_DIST') or 'fedora'
raise Exception('Fedora is not currently supported')
else:
raise Exception('%s is not supported' % distro)
# Do some fiddling to retain answers file support for now
answers_parser = ConfigParser.ConfigParser()
if os.path.isfile(ANSWERS_PATH):
config_answers = StringIO.StringIO()
config_answers.write('[answers]\n')
with open(ANSWERS_PATH) as f:
config_answers.write(f.read())
config_answers.seek(0)
answers_parser.readfp(config_answers)
# Convert conf opts to env values
for opt in _opts:
env_name = opt.name.upper()
if answers_parser.has_option('answers', env_name):
LOG.warning('Using value for %s from instack.answers. This '
'behavior is deprecated. undercloud.conf should '
'now be used for configuration.', env_name)
instack_env[env_name] = answers_parser.get('answers', env_name)
else:
instack_env[env_name] = six.text_type(CONF[opt.name])
# Opts that needs extra processing
if instack_env['DISCOVERY_RUNBENCH'] not in ['0', '1']:
instack_env['DISCOVERY_RUNBENCH'] = ('1' if CONF.discovery_runbench
else '0')
instack_env['PUBLIC_INTERFACE_IP'] = instack_env['LOCAL_IP']
instack_env['LOCAL_IP'] = instack_env['LOCAL_IP'].split('/')[0]
with open(PASSWORD_PATH, 'w') as password_file:
password_file.write('[auth]\n')
for opt in _auth_opts:
env_name = opt.name.upper()
if answers_parser.has_option('answers', env_name):
LOG.warning('Using value for %s from instack.answers. This '
'behavior is deprecated. undercloud.conf should '
'now be used for configuration.', env_name)
value = answers_parser.get('answers', env_name)
else:
value = CONF.auth[opt.name]
if not value:
value = _generate_password()
LOG.info('Generated new password for %s', opt.name)
instack_env[env_name] = value
password_file.write('%s=%s\n' % (opt.name, value))
return instack_env
def _run_instack(instack_env):
args = ['sudo', '-E', 'instack', '-p', instack_env['ELEMENTS_PATH'],
'-j', instack_env['JSONFILE'],
]
LOG.info('Running instack')
_run_live_command(args, instack_env, 'instack')
LOG.info('Instack completed successfully')
def _run_orc(instack_env):
args = ['sudo', 'os-refresh-config']
LOG.info('Running os-refresh-config')
_run_live_command(args, instack_env, 'os-refresh-config')
LOG.info('os-refresh-config completed successfully')
def _extract_from_stackrc(name):
"""Extract authentication values from stackrc
:param name: The value to be extracted. For example: OS_USERNAME or
OS_AUTH_URL.
"""
with open(os.path.expanduser('~/stackrc')) as f:
for line in f:
if name in line:
parts = line.split('=')
return parts[1].rstrip()
def _configure_ssh_keys():
"""Configure default ssh keypair in Nova
Generates a new ssh key for the current user if one does not already
exist, then uploads that to Nova as the 'default' keypair.
"""
id_path = os.path.expanduser('~/.ssh/id_rsa')
if not os.path.isfile(id_path):
args = ['ssh-keygen', '-t', 'rsa', '-N', '', '-f', id_path]
_run_command(args)
LOG.info('Generated new ssh key in ~/.ssh/id_rsa')
args = ['sudo', 'cp', '/root/stackrc', os.path.expanduser('~')]
_run_command(args, name='Copy stackrc')
args = ['sudo', 'chown', getpass.getuser() + ':',
os.path.expanduser('~/stackrc')]
_run_command(args, name='Chown stackrc')
password = _run_command(['hiera', 'admin_password']).rstrip()
user = _extract_from_stackrc('OS_USERNAME')
auth_url = _extract_from_stackrc('OS_AUTH_URL')
tenant = _extract_from_stackrc('OS_TENANT')
nova = novaclient.Client(2, user, password, tenant, auth_url)
try:
nova.keypairs.get('default')
except exceptions.NotFound:
with open(id_path + '.pub') as pubkey:
nova.keypairs.create('default', pubkey.read().rstrip())
def install(instack_root):
"""Install the undercloud
:param instack_root: The path containing the instack-undercloud elements
and json files.
"""
LOG.info('Logging to %s', LOG_FILE)
_load_config()
_check_hostname()
instack_env = _generate_environment(instack_root)
_run_instack(instack_env)
# NOTE(bnemec): I removed the conditional running of os-refresh-config.
# To my knowledge it wasn't really being used anymore, and if we do still
# need it, it should be reimplemented as a client parameter instead of
# an input env var.
# TODO(bnemec): Do we still need INSTACK_ROOT?
instack_env['INSTACK_ROOT'] = os.environ.get('INSTACK_ROOT') or ''
_run_orc(instack_env)
_configure_ssh_keys()
_run_command(['sudo', 'rm', '-f', '/tmp/svc-map-services'], None, 'rm')
LOG.info(COMPLETION_MESSAGE, {'password_path': PASSWORD_PATH,
'stackrc_path': os.path.expanduser('~/stackrc')})

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
six>=1.9.0
python-novaclient
oslo.config

View File

@ -13,9 +13,6 @@ export TRIPLEO_ROOT=${TRIPLEO_ROOT:-"/etc/tripleo"}
export NODES_JSON=${NODES_JSON:-"instackenv.json"}
export TE_DATAFILE=$NODES_JSON
echo "Sourcing answers file from instack.answers..."
source ~/instack.answers
source tripleo-overcloud-passwords
OVERCLOUD_ENDPOINT=$(heat output-show overcloud KeystoneURL|sed 's/^"\(.*\)"$/\1/')

View File

@ -74,9 +74,9 @@ fi
source tripleo-overcloud-passwords
# Undercloud passwords must all be sourced into this script since we make use
# of $UNDERCLOUD_CEILOMETER_SNMPD_PASSWORD below
source tripleo-undercloud-passwords
# NOTE(bnemec): Hopefully this script will eventually be converted to
# Python and then we can kill this sort of hackery.
UNDERCLOUD_CEILOMETER_SNMPD_PASSWORD=$(python -c "import ConfigParser; p = ConfigParser.ConfigParser(); p.read('undercloud-passwords.conf'); print(p.get('auth', 'undercloud_ceilometer_snmpd_password'))")
NeutronFlatNetworks=${NeutronFlatNetworks:-'datacentre'}
NeutronPhysicalBridge=${NeutronPhysicalBridge:-'br-ex'}

View File

@ -1,170 +1,3 @@
#!/bin/bash
set -eu
mkdir -p ~/.instack
LOGFILE=~/.instack/install-undercloud.log
exec > >(tee $LOGFILE)
exec 2>&1
echo "Running $0"
echo "Checking for a FQDN hostname..."
detected_static_hostname=$(sudo hostnamectl --static)
echo "static hostname detected as...$detected_static_hostname"
detected_transient_hostname=$(sudo hostnamectl --transient)
echo "transient hostname detected as...$detected_transient_hostname"
echo "\$HOSTNAME detected as...$HOSTNAME"
if [ ! "$detected_static_hostname" = "$detected_transient_hostname" ]; then
echo "static hostname does not match transient hostname"
echo "Use hostnamectl to set matching hostnames."
exit 1
fi
if [ ! "$detected_static_hostname" = "$HOSTNAME" ]; then
echo "static hostname does not match \$HOSTNAME"
echo "Set \$HOSTNAME to match or relogin so $HOSTNAME is redefined."
exit 1
fi
if ! grep -E "\s+$detected_static_hostname(\s|$)+" /etc/hosts; then
echo "static hostname not set in /etc/hosts"
echo "Please add a line to /etc/hosts for the static hostname."
exit 1
fi
# Attempt to be smart about detecting the path to the json files.
JSONFILEDIR=/usr/share/instack-undercloud/json-files
if [ ! -d "$JSONFILEDIR" ]; then
JSONFILEDIR=$(dirname $0)/../json-files
fi
if $(grep -Eqs 'Red Hat Enterprise Linux' /etc/redhat-release); then
export NODE_DIST=${NODE_DIST:-rhel7}
export JSONFILE=${JSONFILE:-$JSONFILEDIR/rhel-7-undercloud-packages.json}
export REG_METHOD=disable
export REG_HALT_UNREGISTER=1
elif $(grep -Eqs 'CentOS' /etc/redhat-release); then
export NODE_DIST=${NODE_DIST:-centos7}
export JSONFILE=${JSONFILE:-$JSONFILEDIR/centos-7-undercloud-packages.json}
elif $(grep -Eqs 'Fedora' /etc/redhat-release); then
export NODE_DIST=${NODE_DIST:-fedora}
echo "Fedora not currently supported"
exit 1
else
echo "Could not detect distritubion from /etc/redhat-release!"
exit 1
fi
# Attempt to be smart about detecting the patch to the instack-undercloud
# elements
INSTACKUNDERCLOUDELEMENTS=/usr/share/instack-undercloud
if [ ! -d $INSTACKUNDERCLOUDELEMENTS ]; then
INSTACKUNDERCLOUDELEMENTS=$(dirname $0)/../elements
fi
TRIPLEOPUPPETELEMENTS=/usr/share/tripleo-puppet-elements
if [ ! -d $TRIPLEOPUPPETELEMENTS ]; then
# Assume it's checked out in the current directory
TRIPLEOPUPPETELEMENTS=$PWD/tripleo-puppet-elements/elements
fi
export ELEMENTS_PATH=${ELEMENTS_PATH:-"\
$TRIPLEOPUPPETELEMENTS:\
$INSTACKUNDERCLOUDELEMENTS:\
/usr/share/tripleo-image-elements:\
/usr/share/diskimage-builder/elements"}
echo "Sourcing answers file from instack.answers..."
source ~/instack.answers
# Munge up LOCAL_IP.
export PUBLIC_INTERFACE_IP="$LOCAL_IP"
export LOCAL_IP="${LOCAL_IP%/*}"
export IMAGE_PATH
export LOCAL_INTERFACE
export MASQUERADE_NETWORK
export DHCP_START
export DHCP_END
export NETWORK_CIDR
export NETWORK_GATEWAY
export DISCOVERY_INTERFACE
export DISCOVERY_IPRANGE
export DISCOVERY_RUNBENCH
export UNDERCLOUD_DEBUG
# Reuse any existing defined passwords. This is important since any users
# defined in Keystone already have passwords set. So we don't want to generate
# new ones that end up as values in configuration files.
if sudo bash -c '[[ -f /root/tripleo-undercloud-passwords ]]'; then
source <(sudo cat /root/tripleo-undercloud-passwords)
fi
export UNDERCLOUD_HEAT_STACK_DOMAIN_ADMIN_PASSWORD=${UNDERCLOUD_HEAT_STACK_DOMAIN_ADMIN_PASSWORD:-$(tripleo os-make-password)}
export UNDERCLOUD_RABBIT_COOKIE=${UNDERCLOUD_RABBIT_COOKIE:-$(tripleo os-make-password)}
export UNDERCLOUD_RABBIT_PASSWORD=${UNDERCLOUD_RABBIT_PASSWORD:-guest}
export UNDERCLOUD_RABBIT_USERNAME=${UNDERCLOUD_RABBIT_USERNAME:-guest}
export UNDERCLOUD_SWIFT_HASH_SUFFIX=${UNDERCLOUD_SWIFT_HASH_SUFFIX:-$(tripleo os-make-password)}
export UNDERCLOUD_ADMIN_PASSWORD=${UNDERCLOUD_ADMIN_PASSWORD:-$(tripleo os-make-password)}
export UNDERCLOUD_ADMIN_TOKEN=${UNDERCLOUD_ADMIN_TOKEN:-$(tripleo os-make-password)}
export UNDERCLOUD_CEILOMETER_METERING_SECRET=${UNDERCLOUD_CEILOMETER_METERING_SECRET:-$(tripleo os-make-password)}
export UNDERCLOUD_CEILOMETER_PASSWORD=${UNDERCLOUD_CEILOMETER_PASSWORD:-$(tripleo os-make-password)}
export UNDERCLOUD_CEILOMETER_SNMPD_PASSWORD=${UNDERCLOUD_CEILOMETER_SNMPD_PASSWORD:-$(tripleo os-make-password)}
export UNDERCLOUD_CEILOMETER_SNMPD_USER=${UNDERCLOUD_CEILOMETER_SNMPD_USER:-$(tripleo os-make-password)}
export UNDERCLOUD_GLANCE_PASSWORD=${UNDERCLOUD_GLANCE_PASSWORD:-$(tripleo os-make-password)}
export UNDERCLOUD_HEAT_PASSWORD=${UNDERCLOUD_HEAT_PASSWORD:-$(tripleo os-make-password)}
export UNDERCLOUD_HORIZON_SECRET_KEY=${UNDERCLOUD_HORIZON_SECRET_KEY:-$(tripleo os-make-password)}
export UNDERCLOUD_IRONIC_PASSWORD=${UNDERCLOUD_IRONIC_PASSWORD:-$(tripleo os-make-password)}
export UNDERCLOUD_NEUTRON_PASSWORD=${UNDERCLOUD_NEUTRON_PASSWORD:-$(tripleo os-make-password)}
export UNDERCLOUD_NOVA_PASSWORD=${UNDERCLOUD_NOVA_PASSWORD:-$(tripleo os-make-password)}
export UNDERCLOUD_SWIFT_PASSWORD=${UNDERCLOUD_SWIFT_PASSWORD:-$(tripleo os-make-password)}
export UNDERCLOUD_TUSKAR_PASSWORD=${UNDERCLOUD_TUSKAR_PASSWORD:-$(tripleo os-make-password)}
sudo -E instack \
-p $ELEMENTS_PATH \
-j $JSONFILE
RUN_ORC=${RUN_ORC:-"1"}
export INSTACK_ROOT=${INSTACK_ROOT:-""}
if [ "$RUN_ORC" = "1" ]; then
sudo INSTACK_ROOT=$INSTACK_ROOT IMAGE_PATH=$IMAGE_PATH os-refresh-config
# generate ssh authentication keys if they don't exist
if [ ! -f ~/.ssh/id_rsa ]; then
ssh-keygen -t rsa -N "" -f ~/.ssh/id_rsa
fi
source <(sudo cat /root/stackrc)
if ! nova keypair-show default 2>/dev/null; then
tripleo user-config
fi
fi
# This is owned by root, which can break later image builds as a regular user
sudo rm -f /tmp/svc-map-services
export COMPLETION_MESSAGE=${COMPLETION_MESSAGE:-"\
#############################################################################
instack-install-undercloud complete.
The file containing this installation's passwords is at
/root/tripleo-undercloud-passwords.
There is also a stackrc file at /root/stackrc.
These files are needed to interact with the OpenStack services, and should be
secured. For convenience, they can be copied to the current directory:
sudo cp /root/tripleo-undercloud-passwords .
sudo chown $USER: tripleo-undercloud-passwords
sudo cp /root/stackrc .
sudo chown $USER: stackrc
#############################################################################"}
echo "$COMPLETION_MESSAGE"
python -c "from instack_undercloud import undercloud; undercloud.install('$(dirname $0)/..')"

View File

@ -2,9 +2,10 @@
set -eux
instack-create-overcloudrc
source ~/overcloudrc
source ~/instack.answers
# TODO(bnemec): Hard-coding this to . for now because it's tricky to extract
# the value from the new conf file when not using oslo.config.
IMAGE_PATH='.'
# tripleo os-adduser -p $OVERCLOUD_DEMO_PASSWORD demo demo@example.com

View File

@ -11,11 +11,11 @@ export LIBVIRT_DEFAULT_URI="qemu:///system"
if [ ! -d "/usr/share/instack-undercloud" ]; then
DEVTEST_VARIABLES=/usr/libexec/openstack-tripleo/devtest_variables.sh
export ANSWERSFILE=$(dirname $0)/../instack.answers.sample
export ANSWERSFILE=$(dirname $0)/../undercloud.conf.sample
export ELEMENTS_PATH="$(realpath $(dirname $0)/../elements):/usr/share/tripleo-image-elements:/usr/share/diskimage-builder/elements"
else
DEVTEST_VARIABLES=/usr/libexec/openstack-tripleo/devtest_variables.sh
export ANSWERSFILE=/usr/share/instack-undercloud/instack.answers.sample
export ANSWERSFILE=/usr/share/instack-undercloud/undercloud.conf.sample
export ELEMENTS_PATH="/usr/share/instack-undercloud:/usr/share/tripleo-image-elements:/usr/share/diskimage-builder/elements"
fi

View File

@ -20,6 +20,9 @@ classifier =
Programming Language :: Python :: 3.3
[files]
packages =
instack_undercloud
scripts =
scripts/instack-build-images
scripts/instack-create-overcloudrc
@ -35,5 +38,9 @@ data_files =
share/instack-undercloud/heat-templates = heat-templates/*
share/instack-undercloud/json-files = json-files/*
share/instack-undercloud/jenkins-jobs = jenkins-jobs/*
share/instack-undercloud/ = instack.answers.sample
share/instack-undercloud/ = undercloud.conf.sample
share/instack-undercloud/ = deploy-baremetal-overcloudrc
[entry_points]
oslo.config.opts =
instack-undercloud = instack_undercloud.undercloud:list_opts

View File

@ -2,3 +2,15 @@
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
oslosphinx>=2.2.0 # Apache-2.0
sphinx_rtd_theme==0.1.7
hacking>=0.10.0,<0.11
discover
fixtures>=0.3.14
python-subunit>=0.0.18
testrepository>=0.0.18
testscenarios>=0.4
testtools>=0.9.36,!=1.2.0
mock>=1.0
oslotest>=1.5.1 # Apache-2.0
bashate

16
tox.ini
View File

@ -1,12 +1,14 @@
[tox]
minversion = 1.6
skipsdist = True
envlist = py34,py27,pep8
[testenv]
usedevelop = True
install_command = pip install {opts} {packages}
setenv = VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt
commands = python setup.py testr --slowest --testr-args='{posargs}'
[testenv:venv]
commands = {posargs}
@ -16,10 +18,16 @@ commands = sphinx-build -a -t external doc/source/ build/sphinx/html
sphinx-build -a -t internal doc/source/ build/sphinx/internal-html
[testenv:pep8]
deps = bashate
whitelist_externals = bash
commands = bash -c "find scripts -type f | xargs bashate -v"
# We fail pretty horribly on bashate right now
#commands = bash -c "find scripts -type f | xargs bashate -v"
# flake8
commands = flake8
[flake8]
ignore = H803
exclude = .tox
show-source = True
exclude = .tox,dist,doc,*.egg,build
[testenv:genconfig]
commands = oslo-config-generator --config-file config-generator/undercloud.conf

145
undercloud.conf.sample Normal file
View File

@ -0,0 +1,145 @@
[DEFAULT]
#
# From instack-undercloud
#
# Deployment mode for this undercloud. "poc" will allow deployment of
# a single role to heterogenous hardware. "scale" will allow
# deployment of a single role only to homogenous hardware. (string
# value)
# Allowed values: poc, scale
#deployment_mode = poc
# Local file path to the necessary images. The path should be a
# directory readable by the current user that contains the full set of
# images. (string value)
#image_path = .
# IP information for the interface on the Undercloud that will be
# handling the PXE boots and DHCP for Overcloud instances. The IP
# portion of the value will be assigned to the network interface
# defined by local_interface, with the netmask defined by the prefix
# portion of the value. (string value)
#local_ip = 192.0.2.1/24
# Network interface on the Undercloud that will be handling the PXE
# boots and DHCP for Overcloud instances. (string value)
#local_interface = eth1
# Network that will be masqueraded for external access, if required.
# (string value)
#masquerade_network = 192.0.2.0/24
# Start of DHCP allocation range for PXE and DHCP of Overcloud
# instances. (string value)
#dhcp_start = 192.0.2.5
# End of DHCP allocation range for PXE and DHCP of Overcloud
# instances. (string value)
#dhcp_end = 192.0.2.24
# Network CIDR for the Neutron-managed network for Overcloud
# instances. (string value)
#network_cidr = 192.0.2.0/24
# Network gateway for the Neutron-managed network for Overcloud
# instances. (string value)
#network_gateway = 192.0.2.1
# Network interface on which discovery dnsmasq will listen. If in
# doubt, use the default value. (string value)
#discovery_interface = br-ctlplane
# Temporary IP range that will be given to nodes during the discovery
# process. Should not overlap with the range defined by dhcp_start
# and dhcp_end, but should be in the same network. (string value)
#discovery_iprange = 192.0.2.100,192.0.2.120
# Whether to run benchmarks when discovering nodes. (boolean value)
#discovery_runbench = false
# Whether to enable the debug log level for Undercloud OpenStack
# services. (boolean value)
#undercloud_debug = true
[auth]
#
# From instack-undercloud
#
# Password used for MySQL databases. If left unset, one will be
# automatically generated. (string value)
#undercloud_db_password = <None>
# Keystone admin token. If left unset, one will be automatically
# generated. (string value)
#undercloud_admin_token = <None>
# Keystone admin password. If left unset, one will be automatically
# generated. (string value)
#undercloud_admin_password = <None>
# Glance service password. If left unset, one will be automatically
# generated. (string value)
#undercloud_glance_password = <None>
# Heat service password. If left unset, one will be automatically
# generated. (string value)
#undercloud_heat_password = <None>
# Neutron service password. If left unset, one will be automatically
# generated. (string value)
#undercloud_neutron_password = <None>
# Nova service password. If left unset, one will be automatically
# generated. (string value)
#undercloud_nova_password = <None>
# Ironic service password. If left unset, one will be automatically
# generated. (string value)
#undercloud_ironic_password = <None>
# Tuskar service password. If left unset, one will be automatically
# generated. (string value)
#undercloud_tuskar_password = <None>
# Ceilometer service password. If left unset, one will be
# automatically generated. (string value)
#undercloud_ceilometer_password = <None>
# Ceilometer metering secret. If left unset, one will be automatically
# generated. (string value)
#undercloud_ceilometer_metering_secret = <None>
# Ceilometer snmpd user. If left unset, one will be automatically
# generated. (string value)
#undercloud_ceilometer_snmpd_user = <None>
# Ceilometer snmpd password. If left unset, one will be automatically
# generated. (string value)
#undercloud_ceilometer_snmpd_password = <None>
# Swift service password. If left unset, one will be automatically
# generated. (string value)
#undercloud_swift_password = <None>
# Rabbitmq cookie. If left unset, one will be automatically generated.
# (string value)
#undercloud_rabbit_cookie = <None>
# Rabbitmq password. (string value)
#undercloud_rabbit_password = guest
# Rabbitmq username. (string value)
#undercloud_rabbit_username = guest
# Heat stack domain admin password. If left unset, one will be
# automatically generated. (string value)
#undercloud_heat_stack_domain_admin_password = <None>
# Swift hash suffix. If left unset, one will be automatically
# generated. (string value)
#undercloud_swift_hash_suffix = <None>