Better init script
Written in Python. Easier to maintain. Easier to make interactive. Change-Id: Ib579b43c1564b55165de5c2f3d20387122448b19
This commit is contained in:
parent
b5835060ca
commit
93f412fc93
|
@ -55,6 +55,7 @@ nosetests.xml
|
||||||
coverage.xml
|
coverage.xml
|
||||||
*.cover
|
*.cover
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
|
.stestr/
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
*.mo
|
*.mo
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
[DEFAULT]
|
||||||
|
test_path=./tools/init/tests/
|
||||||
|
top_dir=./tools/init/
|
|
@ -3,6 +3,8 @@
|
||||||
parent: openstack-tox-snap-with-sudo
|
parent: openstack-tox-snap-with-sudo
|
||||||
timeout: 7200
|
timeout: 7200
|
||||||
nodeset: ubuntu-bionic
|
nodeset: ubuntu-bionic
|
||||||
|
vars:
|
||||||
|
tox_envlist: init_lint init_unit snap
|
||||||
|
|
||||||
- project:
|
- project:
|
||||||
check:
|
check:
|
||||||
|
|
|
@ -1,317 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "Initializing Microstack."
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Config
|
|
||||||
#
|
|
||||||
# Setup env and templates.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
echo "Loading config and writing out templates ..."
|
|
||||||
|
|
||||||
ospassword=$(snapctl get ospassword)
|
|
||||||
extgateway=$(snapctl get extgateway)
|
|
||||||
extcidr=$(snapctl get extcidr)
|
|
||||||
dns=$(snapctl get dns)
|
|
||||||
|
|
||||||
# Check Config
|
|
||||||
if [ -z "$ospassword" -o -z "$extgateway" -o -z "$dns" -o -z "$extcidr"]; then
|
|
||||||
echo "Missing required config value."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Write out templates and read off of our microstack.rc template
|
|
||||||
# TODO: any password change hooks would go here, updating the password
|
|
||||||
# in the db before writing it to the templates and restarting
|
|
||||||
# services.
|
|
||||||
snap-openstack setup # Write out templates
|
|
||||||
|
|
||||||
# Load openstack .rc into this script's environment. Outside of the
|
|
||||||
# snap shell, this is handled by a wrapper.
|
|
||||||
source $SNAP_COMMON/etc/microstack.rc
|
|
||||||
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# System Optimization
|
|
||||||
#
|
|
||||||
# Perform some tasks that change the host system in ways to better
|
|
||||||
# support microstack.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
# Open up networking so that instances can route to the Internet (see
|
|
||||||
# bin/setup-br-ex for more networking setup, executed on microstack
|
|
||||||
# services start.)
|
|
||||||
echo "Setting up ipv4 forwarding."
|
|
||||||
sudo sysctl net.ipv4.ip_forward=1
|
|
||||||
|
|
||||||
# TODO: add vm swappiness and increased file handle limits here.
|
|
||||||
# TODO: make vm swappiness and file handle changes optional.
|
|
||||||
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# RabbitMQ Setup
|
|
||||||
#
|
|
||||||
# Configure database and wait for services to start.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
echo "Configuring RabbitMQ"
|
|
||||||
|
|
||||||
echo "Waiting for rabbitmq to start"
|
|
||||||
while ! nc -z $extgateway 5672; do sleep 0.1; done;
|
|
||||||
while :;
|
|
||||||
do
|
|
||||||
grep "Starting broker..." ${SNAP_COMMON}/log/rabbitmq/startup_log && \
|
|
||||||
grep "completed" ${SNAP_COMMON}/log/rabbitmq/startup_log && \
|
|
||||||
break
|
|
||||||
sleep 1;
|
|
||||||
done
|
|
||||||
echo "Rabbitmq started."
|
|
||||||
|
|
||||||
# Config!
|
|
||||||
HOME=$SNAP_COMMON/lib/rabbitmq rabbitmqctl add_user openstack rabbitmq || :
|
|
||||||
HOME=$SNAP_COMMON/lib/rabbitmq rabbitmqctl set_permissions openstack ".*" ".*" ".*"
|
|
||||||
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Database setup
|
|
||||||
#
|
|
||||||
# Create databases and initialize keystone.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Wait for MySQL to startup
|
|
||||||
echo "Waiting for MySQL server to start ..."
|
|
||||||
while ! nc -z $extgateway 3306; do sleep 0.1; done;
|
|
||||||
while :;
|
|
||||||
do
|
|
||||||
grep "mysqld: ready for connections." \
|
|
||||||
${SNAP_COMMON}/log/mysql/error.log && break;
|
|
||||||
sleep 1;
|
|
||||||
done
|
|
||||||
echo "Mysql server started."
|
|
||||||
|
|
||||||
for db in neutron nova nova_api nova_cell0 cinder glance keystone; do
|
|
||||||
echo "CREATE DATABASE IF NOT EXISTS ${db}; GRANT ALL PRIVILEGES ON ${db}.* TO '${db}'@'$extgateway' IDENTIFIED BY '${db}';" \
|
|
||||||
| mysql-start-client -u root
|
|
||||||
done
|
|
||||||
|
|
||||||
# Configure Keystone Fernet Keys
|
|
||||||
echo "Configuring Keystone..."
|
|
||||||
snap-openstack launch keystone-manage fernet_setup \
|
|
||||||
--keystone-user root \
|
|
||||||
--keystone-group root
|
|
||||||
snap-openstack launch keystone-manage db_sync
|
|
||||||
|
|
||||||
systemctl restart snap.microstack.keystone-*
|
|
||||||
|
|
||||||
openstack user show admin || {
|
|
||||||
snap-openstack launch keystone-manage bootstrap \
|
|
||||||
--bootstrap-password $ospassword \
|
|
||||||
--bootstrap-admin-url http://$extgateway:5000/v3/ \
|
|
||||||
--bootstrap-internal-url http://$extgateway:5000/v3/ \
|
|
||||||
--bootstrap-public-url http://$extgateway:5000/v3/ \
|
|
||||||
--bootstrap-region-id microstack
|
|
||||||
}
|
|
||||||
|
|
||||||
openstack project show service || {
|
|
||||||
openstack project create --domain default --description "Service Project" service
|
|
||||||
}
|
|
||||||
echo "Keystone configured."
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Nova Setup
|
|
||||||
#
|
|
||||||
# Configure database and wait for services to start.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
echo "Configuring Nova..."
|
|
||||||
|
|
||||||
openstack user show nova || {
|
|
||||||
openstack user create --domain default --password nova nova
|
|
||||||
openstack role add --project service --user nova admin
|
|
||||||
}
|
|
||||||
|
|
||||||
openstack user show placement || {
|
|
||||||
openstack user create --domain default --password placement placement
|
|
||||||
openstack role add --project service --user placement admin
|
|
||||||
}
|
|
||||||
|
|
||||||
openstack service show compute || {
|
|
||||||
openstack service create --name nova \
|
|
||||||
--description "OpenStack Compute" compute
|
|
||||||
|
|
||||||
for endpoint in public internal admin; do
|
|
||||||
openstack endpoint create --region microstack \
|
|
||||||
compute $endpoint http://$extgateway:8774/v2.1 || :
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
openstack service show placement || {
|
|
||||||
openstack service create --name placement \
|
|
||||||
--description "Placement API" placement
|
|
||||||
|
|
||||||
for endpoint in public internal admin; do
|
|
||||||
openstack endpoint create --region microstack \
|
|
||||||
placement $endpoint http://$extgateway:8778 || :
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Grant nova user access to cell0
|
|
||||||
echo "GRANT ALL PRIVILEGES ON nova_cell0.* TO 'nova'@'$extgateway' IDENTIFIED BY 'nova';" \
|
|
||||||
| mysql-start-client -u root
|
|
||||||
|
|
||||||
snap-openstack launch nova-manage api_db sync
|
|
||||||
snap-openstack launch nova-manage cell_v2 list_cells | grep cell0 || {
|
|
||||||
snap-openstack launch nova-manage cell_v2 map_cell0
|
|
||||||
}
|
|
||||||
snap-openstack launch nova-manage cell_v2 list_cells | grep cell1 || {
|
|
||||||
snap-openstack launch nova-manage cell_v2 create_cell --name=cell1 --verbose
|
|
||||||
}
|
|
||||||
snap-openstack launch nova-manage db sync
|
|
||||||
|
|
||||||
systemctl restart snap.microstack.nova-*
|
|
||||||
|
|
||||||
while ! nc -z $extgateway 8774; do sleep 0.1; done;
|
|
||||||
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
openstack flavor show m1.tiny || {
|
|
||||||
openstack flavor create --id 1 --ram 512 --disk 1 --vcpus 1 m1.tiny
|
|
||||||
}
|
|
||||||
openstack flavor show m1.small || {
|
|
||||||
openstack flavor create --id 2 --ram 2048 --disk 20 --vcpus 1 m1.small
|
|
||||||
}
|
|
||||||
openstack flavor show m1.medium || {
|
|
||||||
openstack flavor create --id 3 --ram 4096 --disk 20 --vcpus 2 m1.medium
|
|
||||||
}
|
|
||||||
openstack flavor show m1.large || {
|
|
||||||
openstack flavor create --id 4 --ram 8192 --disk 20 --vcpus 4 m1.large
|
|
||||||
}
|
|
||||||
openstack flavor show m1.xlarge || {
|
|
||||||
openstack flavor create --id 5 --ram 16384 --disk 20 --vcpus 8 m1.xlarge
|
|
||||||
}
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Neutron Setup
|
|
||||||
#
|
|
||||||
# Configure database and wait for services to start.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
echo "Configuring Neutron"
|
|
||||||
|
|
||||||
openstack user show neutron || {
|
|
||||||
openstack user create --domain default --password neutron neutron
|
|
||||||
openstack role add --project service --user neutron admin
|
|
||||||
}
|
|
||||||
|
|
||||||
openstack service show network || {
|
|
||||||
openstack service create --name neutron \
|
|
||||||
--description "OpenStack Network" network
|
|
||||||
|
|
||||||
for endpoint in public internal admin; do
|
|
||||||
openstack endpoint create --region microstack \
|
|
||||||
network $endpoint http://$extgateway:9696 || :
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
snap-openstack launch neutron-db-manage upgrade head
|
|
||||||
|
|
||||||
systemctl restart snap.microstack.neutron-*
|
|
||||||
|
|
||||||
while ! nc -z $extgateway 9696; do sleep 0.1; done;
|
|
||||||
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
openstack network show test || {
|
|
||||||
openstack network create test
|
|
||||||
}
|
|
||||||
|
|
||||||
openstack subnet show test-subnet || {
|
|
||||||
openstack subnet create --network test --subnet-range 192.168.222.0/24 test-subnet
|
|
||||||
}
|
|
||||||
|
|
||||||
openstack network show external || {
|
|
||||||
openstack network create --external \
|
|
||||||
--provider-physical-network=physnet1 \
|
|
||||||
--provider-network-type=flat external
|
|
||||||
}
|
|
||||||
|
|
||||||
openstack subnet show external-subnet || {
|
|
||||||
openstack subnet create --network external --subnet-range 10.20.20.0/24 \
|
|
||||||
--no-dhcp external-subnet
|
|
||||||
}
|
|
||||||
|
|
||||||
openstack router show test-router || {
|
|
||||||
openstack router create test-router
|
|
||||||
openstack router add subnet test-router test-subnet
|
|
||||||
openstack router set --external-gateway external test-router
|
|
||||||
}
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Glance Setup
|
|
||||||
#
|
|
||||||
# Configure database and wait for services to start.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
echo "Configuring Glance"
|
|
||||||
|
|
||||||
openstack user show glance || {
|
|
||||||
openstack user create --domain default --password glance glance
|
|
||||||
openstack role add --project service --user glance admin
|
|
||||||
}
|
|
||||||
|
|
||||||
openstack service show image || {
|
|
||||||
openstack service create --name glance --description "OpenStack Image" image
|
|
||||||
for endpoint in internal admin public; do
|
|
||||||
openstack endpoint create --region microstack \
|
|
||||||
image $endpoint http://$extgateway:9292 || :
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
snap-openstack launch glance-manage db_sync
|
|
||||||
|
|
||||||
systemctl restart snap.microstack.glance*
|
|
||||||
|
|
||||||
while ! nc -z $extgateway 9292; do sleep 0.1; done;
|
|
||||||
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
# Setup the cirros image, which is used by the launch app
|
|
||||||
echo "Grabbing cirros image."
|
|
||||||
openstack image show cirros || {
|
|
||||||
[ -f $SNAP_COMMON/images/cirros-0.4.0-x86_64-disk.img ] || {
|
|
||||||
mkdir -p $SNAP_COMMON/images
|
|
||||||
wget \
|
|
||||||
http://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img \
|
|
||||||
-O ${SNAP_COMMON}/images/cirros-0.4.0-x86_64-disk.img
|
|
||||||
}
|
|
||||||
openstack image create \
|
|
||||||
--file ${SNAP_COMMON}/images/cirros-0.4.0-x86_64-disk.img \
|
|
||||||
--public --container-format=bare --disk-format=qcow2 cirros
|
|
||||||
}
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Post-setup tasks.
|
|
||||||
#
|
|
||||||
# Clean up hanging threads and wait for services to restart.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Restart libvirt and virtlogd to get logging
|
|
||||||
# TODO: figure out why this doesn't Just Work initially
|
|
||||||
systemctl restart snap.microstack.*virt*
|
|
||||||
|
|
||||||
echo "Complete. Marking microstack as initialized!"
|
|
||||||
snapctl set initialized=true
|
|
|
@ -18,6 +18,6 @@ ovs-vsctl --retry --may-exist add-br br-ex
|
||||||
ip address add $extcidr dev br-ex || :
|
ip address add $extcidr dev br-ex || :
|
||||||
ip link set br-ex up || :
|
ip link set br-ex up || :
|
||||||
|
|
||||||
sudo iptables -t nat -A POSTROUTING -s $extcidr ! -d $extcidr -j MASQUERADE
|
sudo iptables -w -t nat -A POSTROUTING -s $extcidr ! -d $extcidr -j MASQUERADE
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|
|
@ -22,7 +22,7 @@ apps:
|
||||||
|
|
||||||
# OpenStack Service Configuration
|
# OpenStack Service Configuration
|
||||||
init:
|
init:
|
||||||
command: init.sh
|
command: microstack_init
|
||||||
# plugs:
|
# plugs:
|
||||||
# - network
|
# - network
|
||||||
|
|
||||||
|
@ -792,3 +792,12 @@ parts:
|
||||||
overlay:
|
overlay:
|
||||||
plugin: dump
|
plugin: dump
|
||||||
source: snap-overlay
|
source: snap-overlay
|
||||||
|
|
||||||
|
# Optionally interactive init script
|
||||||
|
init:
|
||||||
|
plugin: python
|
||||||
|
python-version: python3
|
||||||
|
python-packages:
|
||||||
|
- pymysql
|
||||||
|
- wget
|
||||||
|
source: tools/init
|
||||||
|
|
|
@ -79,7 +79,7 @@ if [ "${UPGRADE_FROM}" != "none" ]; then
|
||||||
$PREFIX sudo snap install --classic --${UPGRADE_FROM} microstack
|
$PREFIX sudo snap install --classic --${UPGRADE_FROM} microstack
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install the snap under test
|
# Install the snap under test -- try again if the machine is not yet ready.
|
||||||
$PREFIX sudo snap install --classic --dangerous microstack*.snap
|
$PREFIX sudo snap install --classic --dangerous microstack*.snap
|
||||||
$PREFIX sudo /snap/bin/microstack.init
|
$PREFIX sudo /snap/bin/microstack.init
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Copyright 2019 Canonical Ltd
|
||||||
|
#
|
||||||
|
# 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.
|
|
@ -0,0 +1,52 @@
|
||||||
|
"""config.py
|
||||||
|
|
||||||
|
Keep track of shared config, logging, etc. here.
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Copyright 2019 Canonical Ltd
|
||||||
|
|
||||||
|
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 logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class Env():
|
||||||
|
"""Singleton that tracks environment variables.
|
||||||
|
|
||||||
|
Contains the env variables of the shell that called us. We also
|
||||||
|
add the snapctl config values to it in the Setup Question.
|
||||||
|
|
||||||
|
"""
|
||||||
|
_global_config = {}
|
||||||
|
_global_config.update(**os.environ)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.__dict__ = self._global_config
|
||||||
|
|
||||||
|
def get_env(self):
|
||||||
|
"""Get a mapping friendly dict."""
|
||||||
|
return self.__dict__
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
# filename='{SNAP_COMMON}/log/microstack_init.log'.format(**Env),
|
||||||
|
stream=sys.stdout,
|
||||||
|
level=logging.DEBUG
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
log = logging # noqa
|
|
@ -0,0 +1,62 @@
|
||||||
|
"""Microstack Init
|
||||||
|
|
||||||
|
Initialize the databases and configuration files of a microstack
|
||||||
|
install.
|
||||||
|
|
||||||
|
We structure our init in the form of 'Question' classes, each of which
|
||||||
|
has an 'ask' routine, run in the order laid out in the
|
||||||
|
question_classes in the main function in this file.
|
||||||
|
|
||||||
|
.ask will either ask the user a question, and run the appropriate
|
||||||
|
routine in the Question class, or simply automatically run a routine
|
||||||
|
without input from the user (in the case of 'required' questions).
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Copyright 2019 Canonical Ltd
|
||||||
|
|
||||||
|
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 sys
|
||||||
|
|
||||||
|
from init.config import log
|
||||||
|
|
||||||
|
from init import questions
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
question_list = [
|
||||||
|
questions.Setup(),
|
||||||
|
questions.IpForwarding(),
|
||||||
|
# The following are not yet implemented:
|
||||||
|
# questions.VmSwappiness(),
|
||||||
|
# questions.FileHandleLimits(),
|
||||||
|
questions.RabbitMQ(),
|
||||||
|
questions.DatabaseSetup(),
|
||||||
|
questions.NovaSetup(),
|
||||||
|
questions.NeutronSetup(),
|
||||||
|
questions.GlanceSetup(),
|
||||||
|
questions.PostSetup(),
|
||||||
|
]
|
||||||
|
|
||||||
|
for question in question_list:
|
||||||
|
try:
|
||||||
|
question.ask()
|
||||||
|
except questions.ConfigError as e:
|
||||||
|
log.critical(e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,129 @@
|
||||||
|
"""question.py
|
||||||
|
|
||||||
|
Contains our Question class, which knows how to ask a question, then
|
||||||
|
run abitrary code.
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Copyright 2019 Canonical Ltd
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidQuestion(Exception):
|
||||||
|
"""Exception to raies in the case where a Question subclass has not
|
||||||
|
been properly implemented.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAnswer(Exception):
|
||||||
|
"""Exception to raise in the case where the user has specified an
|
||||||
|
invalid answer.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerNotImplemented(Exception):
|
||||||
|
"""Exception to raise in the case where a 'yes' or 'no' routine has
|
||||||
|
not been overriden in the subclass, as required.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Question():
|
||||||
|
"""
|
||||||
|
Ask the user a question, and then run code as appropriate.
|
||||||
|
|
||||||
|
Contains a support for always defaulting to yes.
|
||||||
|
|
||||||
|
TODO: Add support for finding answers in a config.yaml.
|
||||||
|
|
||||||
|
"""
|
||||||
|
_valid_types = [
|
||||||
|
'binary', # Yes or No, and variants thereof
|
||||||
|
'string', # Accept (and sanitize) any string
|
||||||
|
'auto' # Don't actually ask a question -- just execute self.yes(True)
|
||||||
|
]
|
||||||
|
|
||||||
|
_question = '(required)'
|
||||||
|
_type = 'auto' # can be binary, string or auto
|
||||||
|
_invalid_prompt = 'Please answer Yes or No.'
|
||||||
|
_retries = 3
|
||||||
|
_input_func = input
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
if self._type not in ['binary', 'string', 'auto']:
|
||||||
|
raise InvalidQuestion(
|
||||||
|
'Invalid type {} specified'.format(self._type))
|
||||||
|
|
||||||
|
def _validate(self, answer: bytes) -> Tuple[str, bool]:
|
||||||
|
"""Validate an answer.
|
||||||
|
|
||||||
|
:param anwser: raw input from the user.
|
||||||
|
|
||||||
|
Returns the answer, and whether or not the answer was valid.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self._type == 'auto':
|
||||||
|
return True, True
|
||||||
|
|
||||||
|
answer = answer.decode('utf8')
|
||||||
|
|
||||||
|
if self._type == 'string':
|
||||||
|
# TODO Santize this!
|
||||||
|
return answer, True
|
||||||
|
|
||||||
|
# self._type is binary
|
||||||
|
if answer.lower() in ['y', 'yes']:
|
||||||
|
return True, True
|
||||||
|
|
||||||
|
if answer.lower() in ['n', 'no']:
|
||||||
|
return False, True
|
||||||
|
|
||||||
|
return answer, False
|
||||||
|
|
||||||
|
def yes(self, answer: str) -> None:
|
||||||
|
"""Routine to run if the user answers 'yes' or with a value."""
|
||||||
|
raise AnswerNotImplemented('You must override this method.')
|
||||||
|
|
||||||
|
def no(self, answer: str) -> None:
|
||||||
|
"""Routine to run if the user answers 'no'"""
|
||||||
|
raise AnswerNotImplemented('You must override this method.')
|
||||||
|
|
||||||
|
def ask(self) -> None:
|
||||||
|
"""
|
||||||
|
Ask the user a question.
|
||||||
|
|
||||||
|
Run self.yes or self.no as appropriate. Raise an error if the
|
||||||
|
user cannot specify a valid answer after self._retries tries.
|
||||||
|
|
||||||
|
"""
|
||||||
|
prompt = self._question
|
||||||
|
|
||||||
|
for i in range(0, self._retries):
|
||||||
|
awr, valid = self._validate(
|
||||||
|
self._type == 'auto' or self._input_func(prompt))
|
||||||
|
if valid:
|
||||||
|
if awr:
|
||||||
|
return self.yes(awr)
|
||||||
|
return self.no(awr)
|
||||||
|
prompt = '{} is not valid. {}'.format(awr, self._invalid_prompt)
|
||||||
|
|
||||||
|
raise InvalidAnswer('Too many invalid answers.')
|
|
@ -0,0 +1,410 @@
|
||||||
|
"""questions.py
|
||||||
|
|
||||||
|
All of our subclasses of Question live here.
|
||||||
|
|
||||||
|
We might break this file up into multiple pieces at some point, but
|
||||||
|
for now, we're keeping things simple (if a big lengthy)
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Copyright 2019 Canonical Ltd
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from time import sleep
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
from init.shell import (check, call, check_output, shell, sql, nc_wait,
|
||||||
|
log_wait, restart, download)
|
||||||
|
from init.config import Env, log
|
||||||
|
from init.question import Question
|
||||||
|
|
||||||
|
|
||||||
|
_env = Env().get_env()
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigError(Exception):
|
||||||
|
"""Suitable error to raise in case there is an issue with the snapctl
|
||||||
|
config or environment vars.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Setup(Question):
|
||||||
|
"""Prepare our environment.
|
||||||
|
|
||||||
|
Check to make sure that everything is in place, and populate our
|
||||||
|
config object and os.environ with the correct values.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def yes(self, answer: str) -> None:
|
||||||
|
"""Since this is an auto question, we always execute yes."""
|
||||||
|
log.info('Loading config and writing templates ...')
|
||||||
|
|
||||||
|
log.info('Validating config ...')
|
||||||
|
for key in ['ospassword', 'extgateway', 'extcidr', 'dns']:
|
||||||
|
val = check_output('snapctl', 'get', key)
|
||||||
|
if not val:
|
||||||
|
raise ConfigError(
|
||||||
|
'Expected config value {} is not set.'.format(key))
|
||||||
|
_env[key] = val
|
||||||
|
|
||||||
|
log.info('Writing out templates ...')
|
||||||
|
check('snap-openstack', 'setup')
|
||||||
|
|
||||||
|
# Parse microstack.rc, and load into _env
|
||||||
|
# TODO: write something more robust (this breaks on comments
|
||||||
|
# at end of line.)
|
||||||
|
mstackrc = '{SNAP_COMMON}/etc/microstack.rc'.format(**_env)
|
||||||
|
with open(mstackrc, 'r') as rc_file:
|
||||||
|
for line in rc_file.readlines():
|
||||||
|
if not line.startswith('export'):
|
||||||
|
continue
|
||||||
|
key, val = line[7:].split('=')
|
||||||
|
_env[key.strip()] = val.strip()
|
||||||
|
|
||||||
|
|
||||||
|
class IpForwarding(Question):
|
||||||
|
"""Possibly setup IP forwarding."""
|
||||||
|
|
||||||
|
_type = 'auto' # Auto for now, to maintain old behavior.
|
||||||
|
_question = 'Do you wish to setup ip forwarding? (recommended)'
|
||||||
|
|
||||||
|
def yes(self, answer: str) -> None:
|
||||||
|
"""Use sysctl to setup ip forwarding."""
|
||||||
|
log.info('Setting up ipv4 forwarding...')
|
||||||
|
|
||||||
|
check('sysctl', 'net.ipv4.ip_forward=1')
|
||||||
|
|
||||||
|
|
||||||
|
class VmSwappiness(Question):
|
||||||
|
|
||||||
|
_type = 'binary'
|
||||||
|
_question = 'Do you wish to set vm swappiness to 1? (recommended)'
|
||||||
|
|
||||||
|
def yes(self, answer: str) -> None:
|
||||||
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FileHandleLimits(Question):
|
||||||
|
|
||||||
|
_type = 'binary'
|
||||||
|
_question = 'Do you wish to increase file handle limits? (recommended)'
|
||||||
|
|
||||||
|
def yes(self, answer: str) -> None:
|
||||||
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RabbitMQ(Question):
|
||||||
|
"""Wait for Rabbit to start, then setup permissions."""
|
||||||
|
|
||||||
|
def _wait(self) -> None:
|
||||||
|
nc_wait(_env['extgateway'], '5672')
|
||||||
|
log_file = '{SNAP_COMMON}/log/rabbitmq/startup_log'.format(**_env)
|
||||||
|
log_wait(log_file, 'completed')
|
||||||
|
|
||||||
|
def _configure(self) -> None:
|
||||||
|
"""Configure RabbitMQ
|
||||||
|
|
||||||
|
(actions may have already been run, in which case we fail silently).
|
||||||
|
"""
|
||||||
|
# Add Erlang HOME to env.
|
||||||
|
env = dict(**_env)
|
||||||
|
env['HOME'] = '{SNAP_COMMON}/lib/rabbitmq'.format(**_env)
|
||||||
|
# Configure RabbitMQ
|
||||||
|
call('rabbitmqctl', 'add_user', 'openstack', 'rabbitmq', env=env)
|
||||||
|
shell('rabbitmqctl set_permissions openstack ".*" ".*" ".*"', env=env)
|
||||||
|
|
||||||
|
def yes(self, answer: str) -> None:
|
||||||
|
log.info('Waiting for RabbitMQ to start ...')
|
||||||
|
self._wait()
|
||||||
|
log.info('RabbitMQ started!')
|
||||||
|
log.info('Configuring RabbitMQ ...')
|
||||||
|
self._configure()
|
||||||
|
log.info('RabbitMQ Configured!')
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseSetup(Question):
|
||||||
|
"""Setup keystone permissions, then setup all databases."""
|
||||||
|
|
||||||
|
def _wait(self) -> None:
|
||||||
|
nc_wait(_env['extgateway'], '3306')
|
||||||
|
log_wait('{SNAP_COMMON}/log/mysql/error.log'.format(**_env),
|
||||||
|
'mysqld: ready for connections.')
|
||||||
|
|
||||||
|
def _create_dbs(self) -> None:
|
||||||
|
for db in ('neutron', 'nova', 'nova_api', 'nova_cell0', 'cinder',
|
||||||
|
'glance', 'keystone'):
|
||||||
|
sql("CREATE DATABASE IF NOT EXISTS {db};".format(db=db))
|
||||||
|
sql(
|
||||||
|
"GRANT ALL PRIVILEGES ON {db}.* TO {db}@{extgateway} \
|
||||||
|
IDENTIFIED BY '{db}';".format(db=db, **_env))
|
||||||
|
|
||||||
|
def _bootstrap(self) -> None:
|
||||||
|
|
||||||
|
if call('openstack', 'user', 'show', 'admin'):
|
||||||
|
return
|
||||||
|
|
||||||
|
bootstrap_url = 'http://{extgateway}:5000/v3/'.format(**_env)
|
||||||
|
|
||||||
|
check('snap-openstack', 'launch', 'keystone-manage', 'bootstrap',
|
||||||
|
'--bootstrap-password', _env['ospassword'],
|
||||||
|
'--bootstrap-admin-url', bootstrap_url,
|
||||||
|
'--bootstrap-internal-url', bootstrap_url,
|
||||||
|
'--bootstrap-public-url', bootstrap_url)
|
||||||
|
|
||||||
|
def yes(self, answer: str) -> None:
|
||||||
|
"""Setup Databases.
|
||||||
|
|
||||||
|
Create all the MySQL databases we require, then setup the
|
||||||
|
fernet keys and create the service project.
|
||||||
|
|
||||||
|
"""
|
||||||
|
log.info('Waiting for MySQL server to start ...')
|
||||||
|
self._wait()
|
||||||
|
log.info('Mysql server started! Creating databases ...')
|
||||||
|
self._create_dbs()
|
||||||
|
|
||||||
|
log.info('Configuring Keystone Fernet Keys ...')
|
||||||
|
check('snap-openstack', 'launch', 'keystone-manage',
|
||||||
|
'fernet_setup', '--keystone-user', 'root',
|
||||||
|
'--keystone-group', 'root')
|
||||||
|
check('snap-openstack', 'launch', 'keystone-manage', 'db_sync')
|
||||||
|
|
||||||
|
restart('keystone-*')
|
||||||
|
|
||||||
|
log.info('Bootstrapping Keystone ...')
|
||||||
|
self._bootstrap()
|
||||||
|
|
||||||
|
log.info('Creating service project ...')
|
||||||
|
if not call('openstack', 'project', 'show', 'service'):
|
||||||
|
check('openstack', 'project', 'create', '--domain',
|
||||||
|
'default', '--description', 'Service Project',
|
||||||
|
'service')
|
||||||
|
|
||||||
|
log.info('Keystone configured!')
|
||||||
|
|
||||||
|
|
||||||
|
class NovaSetup(Question):
|
||||||
|
"""Create all relevant nova users and services."""
|
||||||
|
|
||||||
|
def _flavors(self) -> None:
|
||||||
|
"""Create default flavors."""
|
||||||
|
|
||||||
|
if not call('openstack', 'flavor', 'show', 'm1.tiny'):
|
||||||
|
check('openstack', 'flavor', 'create', '--id', '1',
|
||||||
|
'--ram', '512', '--disk', '1', '--vcpus', '1', 'm1.tiny')
|
||||||
|
if not call('openstack', 'flavor', 'show', 'm1.small'):
|
||||||
|
check('openstack', 'flavor', 'create', '--id', '2',
|
||||||
|
'--ram', '2048', '--disk', '20', '--vcpus', '1', 'm1.small')
|
||||||
|
if not call('openstack', 'flavor', 'show', 'm1.medium'):
|
||||||
|
check('openstack', 'flavor', 'create', '--id', '3',
|
||||||
|
'--ram', '4096', '--disk', '20', '--vcpus', '2', 'm1.medium')
|
||||||
|
if not call('openstack', 'flavor', 'show', 'm1.large'):
|
||||||
|
check('openstack', 'flavor', 'create', '--id', '4',
|
||||||
|
'--ram', '8192', '--disk', '20', '--vcpus', '4', 'm1.large')
|
||||||
|
if not call('openstack', 'flavor', 'show', 'm1.xlarge'):
|
||||||
|
check('openstack', 'flavor', 'create', '--id', '5',
|
||||||
|
'--ram', '16384', '--disk', '20', '--vcpus', '8',
|
||||||
|
'm1.xlarge')
|
||||||
|
|
||||||
|
def yes(self, answer: str) -> None:
|
||||||
|
log.info('Configuring nova ...')
|
||||||
|
|
||||||
|
if not call('openstack', 'user', 'show', 'nova'):
|
||||||
|
check('openstack', 'user', 'create', '--domain',
|
||||||
|
'default', '--password', 'nova', 'nova')
|
||||||
|
check('openstack', 'role', 'add', '--project',
|
||||||
|
'service', '--user', 'nova', 'admin')
|
||||||
|
|
||||||
|
if not call('openstack', 'user', 'show', 'placement'):
|
||||||
|
check('openstack', 'user', 'create', '--domain', 'default',
|
||||||
|
'--password', 'placement', 'placement')
|
||||||
|
check('openstack', 'role', 'add', '--project', 'service',
|
||||||
|
'--user', 'placement', 'admin')
|
||||||
|
|
||||||
|
if not call('openstack', 'service', 'show', 'compute'):
|
||||||
|
check('openstack', 'service', 'create', '--name', 'nova',
|
||||||
|
'--description', '"Openstack Compute"', 'compute')
|
||||||
|
for endpoint in ['public', 'internal', 'admin']:
|
||||||
|
call('openstack', 'endpoint', 'create', '--region',
|
||||||
|
'microstack', 'compute', endpoint,
|
||||||
|
'http://{extgateway}:8774/v2.1'.format(**_env))
|
||||||
|
|
||||||
|
if not call('openstack', 'service', 'show', 'placement'):
|
||||||
|
check('openstack', 'service', 'create', '--name',
|
||||||
|
'placement', '--description', '"Placement API"',
|
||||||
|
'placement')
|
||||||
|
|
||||||
|
for endpoint in ['public', 'internal', 'admin']:
|
||||||
|
call('openstack', 'endpoint', 'create', '--region',
|
||||||
|
'microstack', 'placement', endpoint,
|
||||||
|
'http://{extgateway}:8778'.format(**_env))
|
||||||
|
|
||||||
|
# Grant nova user access to cell0
|
||||||
|
sql(
|
||||||
|
"GRANT ALL PRIVILEGES ON nova_cell0.* TO 'nova'@'{extgateway}' \
|
||||||
|
IDENTIFIED BY \'nova';".format(**_env))
|
||||||
|
|
||||||
|
check('snap-openstack', 'launch', 'nova-manage', 'api_db', 'sync')
|
||||||
|
|
||||||
|
if 'cell0' not in check_output('snap-openstack', 'launch',
|
||||||
|
'nova-manage', 'cell_v2',
|
||||||
|
'list_cells'):
|
||||||
|
check('snap-openstack', 'launch', 'nova-manage',
|
||||||
|
'cell_v2', 'map_cell0')
|
||||||
|
|
||||||
|
if 'cell1' not in check_output('snap-openstack', 'launch',
|
||||||
|
'nova-manage', 'cell_v2', 'list_cells'):
|
||||||
|
|
||||||
|
check('snap-openstack', 'launch', 'nova-manage', 'cell_v2',
|
||||||
|
'create_cell', '--name=cell1', '--verbose')
|
||||||
|
|
||||||
|
check('snap-openstack', 'launch', 'nova-manage', 'db', 'sync')
|
||||||
|
|
||||||
|
restart('nova-*')
|
||||||
|
|
||||||
|
nc_wait(_env['extgateway'], '8774')
|
||||||
|
|
||||||
|
sleep(5) # TODO: log_wait
|
||||||
|
|
||||||
|
log.info('Creating default flavors...')
|
||||||
|
self._flavors()
|
||||||
|
|
||||||
|
|
||||||
|
class NeutronSetup(Question):
|
||||||
|
"""Create all relevant neutron services and users."""
|
||||||
|
|
||||||
|
def yes(self, answer: str) -> None:
|
||||||
|
log.info('Configuring Neutron')
|
||||||
|
|
||||||
|
if not call('openstack', 'user', 'show', 'neutron'):
|
||||||
|
check('openstack', 'user', 'create', '--domain', 'default',
|
||||||
|
'--password', 'neutron', 'neutron')
|
||||||
|
check('openstack', 'role', 'add', '--project', 'service',
|
||||||
|
'--user', 'neutron', 'admin')
|
||||||
|
|
||||||
|
if not call('openstack', 'service', 'show', 'network'):
|
||||||
|
check('openstack', 'service', 'create', '--name', 'neutron',
|
||||||
|
'--description', '"OpenStack Network"', 'network')
|
||||||
|
for endpoint in ['public', 'internal', 'admin']:
|
||||||
|
call('openstack', 'endpoint', 'create', '--region',
|
||||||
|
'microstack', 'network', endpoint,
|
||||||
|
'http://{extgateway}:9696'.format(**_env))
|
||||||
|
|
||||||
|
check('snap-openstack', 'launch', 'neutron-db-manage', 'upgrade',
|
||||||
|
'head')
|
||||||
|
|
||||||
|
restart('neutron-*')
|
||||||
|
|
||||||
|
nc_wait(_env['extgateway'], '9696')
|
||||||
|
|
||||||
|
sleep(5) # TODO: log_wait
|
||||||
|
|
||||||
|
if not call('openstack', 'network', 'show', 'test'):
|
||||||
|
check('openstack', 'network', 'create', 'test')
|
||||||
|
|
||||||
|
if not call('openstack', 'subnet', 'show', 'test-subnet'):
|
||||||
|
check('openstack', 'subnet', 'create', '--network', 'test',
|
||||||
|
'--subnet-range', '192.168.222.0/24', 'test-subnet')
|
||||||
|
|
||||||
|
if not call('openstack', 'network', 'show', 'external'):
|
||||||
|
check('openstack', 'network', 'create', '--external',
|
||||||
|
'--provider-physical-network=physnet1',
|
||||||
|
'--provider-network-type=flat', 'external')
|
||||||
|
if not call('openstack', 'subnet', 'show', 'external-subnet'):
|
||||||
|
check('openstack', 'subnet', 'create', '--network', 'external',
|
||||||
|
'--subnet-range', _env['extcidr'], '--no-dhcp',
|
||||||
|
'external-subnet')
|
||||||
|
|
||||||
|
if not call('openstack', 'router', 'show', 'test-router'):
|
||||||
|
check('openstack', 'router', 'create', 'test-router')
|
||||||
|
check('openstack', 'router', 'add', 'subnet', 'test-router',
|
||||||
|
'test-subnet')
|
||||||
|
check('openstack', 'router', 'set', '--external-gateway',
|
||||||
|
'external', 'test-router')
|
||||||
|
|
||||||
|
|
||||||
|
class GlanceSetup(Question):
|
||||||
|
"""Setup glance, and download an initial Cirros image."""
|
||||||
|
|
||||||
|
def _fetch_cirros(self) -> None:
|
||||||
|
|
||||||
|
if call('openstack', 'image', 'show', 'cirros'):
|
||||||
|
return
|
||||||
|
|
||||||
|
env = dict(**_env)
|
||||||
|
env['VER'] = '0.4.0'
|
||||||
|
env['IMG'] = 'cirros-{VER}-x86_64-disk.img'.format(**env)
|
||||||
|
|
||||||
|
log.info('Fetching cirros image ...')
|
||||||
|
|
||||||
|
cirros_path = '{SNAP_COMMON}/images/{IMG}'.format(**env)
|
||||||
|
|
||||||
|
if not path.exists(cirros_path):
|
||||||
|
check('mkdir', '-p', '{SNAP_COMMON}/images'.format(**env))
|
||||||
|
download(
|
||||||
|
'http://download.cirros-cloud.net/{VER}/{IMG}'.format(**env),
|
||||||
|
'{SNAP_COMMON}/images/{IMG}'.format(**env))
|
||||||
|
|
||||||
|
check('openstack', 'image', 'create', '--file',
|
||||||
|
'{SNAP_COMMON}/images/{IMG}'.format(**env),
|
||||||
|
'--public', '--container-format=bare',
|
||||||
|
'--disk-format=qcow2', 'cirros')
|
||||||
|
|
||||||
|
def yes(self, answer: str) -> None:
|
||||||
|
|
||||||
|
log.info('Configuring Glance ...')
|
||||||
|
|
||||||
|
if not call('openstack', 'user', 'show', 'glance'):
|
||||||
|
check('openstack', 'user', 'create', '--domain', 'default',
|
||||||
|
'--password', 'glance', 'glance')
|
||||||
|
check('openstack', 'role', 'add', '--project', 'service',
|
||||||
|
'--user', 'glance', 'admin')
|
||||||
|
|
||||||
|
if not call('openstack', 'service', 'show', 'image'):
|
||||||
|
check('openstack', 'service', 'create', '--name', 'glance',
|
||||||
|
'--description', '"OpenStack Image"', 'image')
|
||||||
|
for endpoint in ['internal', 'admin', 'public']:
|
||||||
|
check('openstack', 'endpoint', 'create', '--region',
|
||||||
|
'microstack', 'image', endpoint,
|
||||||
|
'http://{extgateway}:9292'.format(**_env))
|
||||||
|
|
||||||
|
check('snap-openstack', 'launch', 'glance-manage', 'db_sync')
|
||||||
|
|
||||||
|
restart('glance*')
|
||||||
|
|
||||||
|
nc_wait(_env['extgateway'], '9292')
|
||||||
|
|
||||||
|
sleep(5) # TODO: log_wait
|
||||||
|
|
||||||
|
self._fetch_cirros()
|
||||||
|
|
||||||
|
|
||||||
|
class PostSetup(Question):
|
||||||
|
"""Sneak in any additional cleanup, then set the initialized state."""
|
||||||
|
|
||||||
|
def yes(self, answer: str) -> None:
|
||||||
|
|
||||||
|
log.info('restarting libvirt and virtlogd ...')
|
||||||
|
# This fixes an issue w/ logging not getting set.
|
||||||
|
# TODO: fix issue.
|
||||||
|
restart('*virt*')
|
||||||
|
|
||||||
|
check('snapctl', 'set', 'initialized=true')
|
||||||
|
log.info('Complete. Marked microstack as initialized!')
|
|
@ -0,0 +1,136 @@
|
||||||
|
"""shell.py
|
||||||
|
|
||||||
|
Contains wrappers around subprocess and pymysql commands, specific to
|
||||||
|
our needs in the init script.
|
||||||
|
|
||||||
|
# TODO capture stdout (and output to log.DEBUG)
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Copyright 2019 Canonical Ltd
|
||||||
|
|
||||||
|
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 subprocess
|
||||||
|
from time import sleep
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
import pymysql
|
||||||
|
import wget
|
||||||
|
|
||||||
|
from init.config import Env
|
||||||
|
|
||||||
|
|
||||||
|
_env = Env().get_env()
|
||||||
|
|
||||||
|
|
||||||
|
def check(*args: List[str], env: Dict = _env) -> int:
|
||||||
|
"""Execute a shell command, raising an error on failed excution.
|
||||||
|
|
||||||
|
:param args: strings to be composed into the bash call.
|
||||||
|
:param env: defaults to our Env singleton; can be overriden.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return subprocess.check_call(args, env=env)
|
||||||
|
|
||||||
|
|
||||||
|
def check_output(*args: List[str], env: Dict = _env) -> str:
|
||||||
|
"""Execute a shell command, returning the output of the command.
|
||||||
|
|
||||||
|
:param args: strings to be composed into the bash call.
|
||||||
|
:param env: defaults to our Env singleton; can be overriden.
|
||||||
|
|
||||||
|
Include our env; pass in any extra keyword args.
|
||||||
|
"""
|
||||||
|
return subprocess.check_output(args, env=env,
|
||||||
|
universal_newlines=True).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def call(*args: List[str], env: Dict = _env) -> bool:
|
||||||
|
"""Execute a shell command.
|
||||||
|
|
||||||
|
Return True if the call executed successfully (returned 0), or
|
||||||
|
False if it returned with an error code (return > 0)
|
||||||
|
|
||||||
|
:param args: strings to be composed into the bash call.
|
||||||
|
:param env: defaults to our Env singleton; can be overriden.
|
||||||
|
"""
|
||||||
|
return not subprocess.call(args, env=env)
|
||||||
|
|
||||||
|
|
||||||
|
def shell(cmd: str, env: Dict = _env) -> int:
|
||||||
|
"""Execute a command, using the actual bourne again shell.
|
||||||
|
|
||||||
|
Use this in cases where it is difficult to compose a comma
|
||||||
|
separate list that will get parsed into a succesful bash
|
||||||
|
command. (E.g., your bash command contains an argument like ".*"
|
||||||
|
".*" ".*")
|
||||||
|
|
||||||
|
:param cmd: the command to run.
|
||||||
|
:param env: defaults to our Env singleton; can be overriden.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return subprocess.check_call(cmd, shell=True, env=env,
|
||||||
|
executable='/snap/core18/current/bin/bash')
|
||||||
|
|
||||||
|
|
||||||
|
def sql(cmd: str) -> None:
|
||||||
|
"""Execute some SQL!
|
||||||
|
|
||||||
|
Really simply wrapper around a pymysql connection, suitable for
|
||||||
|
passing the limited CREATE and GRANT commands that we need to pass
|
||||||
|
in our init script.
|
||||||
|
|
||||||
|
:param cmd: sql to execute.
|
||||||
|
|
||||||
|
"""
|
||||||
|
mysql_conf = '${SNAP_USER_COMMON}/etc/mysql/my.cnf'.format(**_env)
|
||||||
|
connection = pymysql.connect(host='localhost', user='root',
|
||||||
|
read_default_file=mysql_conf)
|
||||||
|
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def nc_wait(addr: str, port: str) -> None:
|
||||||
|
"""Wait for a service to be answering on a port."""
|
||||||
|
print('Waiting for {}:{}'.format(addr, port))
|
||||||
|
while not call('nc', '-z', addr, port):
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
def log_wait(log: str, message: str) -> None:
|
||||||
|
"""Wait until a message appears in a log."""
|
||||||
|
while True:
|
||||||
|
with open(log, 'r') as log_file:
|
||||||
|
for line in log_file.readlines():
|
||||||
|
if message in line:
|
||||||
|
return
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
def restart(service: str) -> None:
|
||||||
|
"""Restart a microstack service.
|
||||||
|
|
||||||
|
:param service: the service(s) to be restarted. Can contain wild cards.
|
||||||
|
e.g. *rabbit*
|
||||||
|
|
||||||
|
"""
|
||||||
|
check('systemctl', 'restart', 'snap.microstack.{}'.format(service))
|
||||||
|
|
||||||
|
|
||||||
|
def download(url: str, output: str) -> None:
|
||||||
|
"""Download a file to a path"""
|
||||||
|
wget.download(url, output)
|
|
@ -0,0 +1,2 @@
|
||||||
|
pymysql
|
||||||
|
wget
|
|
@ -0,0 +1,13 @@
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="microstack_init",
|
||||||
|
description="Optionally interactive init script for Microstack.",
|
||||||
|
packages=find_packages(),
|
||||||
|
version="0.0.1",
|
||||||
|
entry_points={
|
||||||
|
'console_scripts': [
|
||||||
|
'microstack_init = init.main:main',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
|
@ -0,0 +1,4 @@
|
||||||
|
pylint
|
||||||
|
pep8
|
||||||
|
stestr
|
||||||
|
flake8
|
|
@ -0,0 +1,125 @@
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
# TODO: drop in test runner and get rid of this line.
|
||||||
|
sys.path.append(os.getcwd()) # noqa
|
||||||
|
|
||||||
|
from init.question import (Question, InvalidQuestion, InvalidAnswer,
|
||||||
|
AnswerNotImplemented)
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Test Fixtures
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidTypeQuestion(Question):
|
||||||
|
_type = 'foo'
|
||||||
|
|
||||||
|
|
||||||
|
class IncompleteQuestion(Question):
|
||||||
|
_type = 'auto'
|
||||||
|
|
||||||
|
|
||||||
|
class GoodAutoQuestion(Question):
|
||||||
|
_type = 'auto'
|
||||||
|
|
||||||
|
def yes(self, answer):
|
||||||
|
return 'I am a good question!'
|
||||||
|
|
||||||
|
|
||||||
|
class GoodBinaryQuestion(Question):
|
||||||
|
_type = 'binary'
|
||||||
|
|
||||||
|
def yes(self, answer):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def no(self, answer):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class GoodStringQuestion(Question):
|
||||||
|
"""Pass a string through to the output of Question.ask.
|
||||||
|
|
||||||
|
# TODO right now, we have separate handlers for Truthy and Falsey
|
||||||
|
answers, and this test class basically makes them do the same
|
||||||
|
thing. Is this a good pattern?
|
||||||
|
|
||||||
|
"""
|
||||||
|
_type = 'string'
|
||||||
|
|
||||||
|
def yes(self, answer):
|
||||||
|
return answer
|
||||||
|
|
||||||
|
def no(self, answer):
|
||||||
|
return answer
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Tests Proper
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
class TestQuestionClass(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Test basic features of the Question class.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def test_invalid_type(self):
|
||||||
|
|
||||||
|
with self.assertRaises(InvalidQuestion):
|
||||||
|
InvalidTypeQuestion().ask()
|
||||||
|
|
||||||
|
def test_valid_type(self):
|
||||||
|
|
||||||
|
self.assertTrue(GoodBinaryQuestion())
|
||||||
|
|
||||||
|
def test_not_implemented(self):
|
||||||
|
|
||||||
|
with self.assertRaises(AnswerNotImplemented):
|
||||||
|
IncompleteQuestion().ask()
|
||||||
|
|
||||||
|
def test_auto_question(self):
|
||||||
|
|
||||||
|
self.assertEqual(GoodAutoQuestion().ask(), 'I am a good question!')
|
||||||
|
|
||||||
|
|
||||||
|
class TestInput(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Test input handling.
|
||||||
|
|
||||||
|
Takes advantage of the fact that we can override the Question
|
||||||
|
class's input handler.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def test_binary_question(self):
|
||||||
|
|
||||||
|
q = GoodBinaryQuestion()
|
||||||
|
|
||||||
|
for answer in ['yes', 'Yes', 'y']:
|
||||||
|
q._input_func = lambda x: answer.encode('utf8')
|
||||||
|
self.assertTrue(q.ask())
|
||||||
|
|
||||||
|
for answer in ['No', 'n', 'no']:
|
||||||
|
q._input_func = lambda x: answer.encode('utf8')
|
||||||
|
self.assertFalse(q.ask())
|
||||||
|
|
||||||
|
with self.assertRaises(InvalidAnswer):
|
||||||
|
q._input_func = lambda x: 'foo'.encode('utf8')
|
||||||
|
q.ask()
|
||||||
|
|
||||||
|
def test_string_question(self):
|
||||||
|
q = GoodStringQuestion()
|
||||||
|
|
||||||
|
for answer in ['foo', 'bar', 'baz', '', 'yadayadayada']:
|
||||||
|
q._input_func = lambda x: answer.encode('utf8')
|
||||||
|
self.assertEqual(answer, q.ask())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
15
tox.ini
15
tox.ini
|
@ -1,5 +1,5 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist = multipass
|
envlist = init_lint, init_unit, multipass
|
||||||
skipsdist = True
|
skipsdist = True
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
@ -36,3 +36,16 @@ commands =
|
||||||
# Just run basic_test.sh, with multipass support.
|
# Just run basic_test.sh, with multipass support.
|
||||||
commands =
|
commands =
|
||||||
{toxinidir}/tests/basic-test.sh -m
|
{toxinidir}/tests/basic-test.sh -m
|
||||||
|
|
||||||
|
|
||||||
|
[testenv:init_lint]
|
||||||
|
basepython=python3
|
||||||
|
deps = -r{toxinidir}/tools/init/test-requirements.txt
|
||||||
|
-r{toxinidir}/tools/init/requirements.txt
|
||||||
|
commands = flake8 {toxinidir}/tools/init/init/
|
||||||
|
|
||||||
|
[testenv:init_unit]
|
||||||
|
basepython=python3
|
||||||
|
deps = -r{toxinidir}/tools/init/test-requirements.txt
|
||||||
|
-r{toxinidir}/tools/init/requirements.txt
|
||||||
|
commands = stestr run {posargs}
|
||||||
|
|
Loading…
Reference in New Issue