Replace master with feature/zuulv3
Change-Id: I99650ec1637f7864829600ec0e8feb11a5350c53
This commit is contained in:
commit
46706ae06b
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,6 +12,5 @@ doc/build/*
|
||||
zuul/versioninfo
|
||||
dist/
|
||||
venv/
|
||||
nodepool.yaml
|
||||
*~
|
||||
.*.swp
|
||||
|
@ -2,3 +2,4 @@
|
||||
host=review.openstack.org
|
||||
port=29418
|
||||
project=openstack-infra/nodepool.git
|
||||
|
||||
|
48
.zuul.yaml
48
.zuul.yaml
@ -1,26 +1,3 @@
|
||||
- job:
|
||||
name: nodepool-functional
|
||||
parent: legacy-dsvm-base
|
||||
run: playbooks/nodepool-functional/run.yaml
|
||||
post-run: playbooks/nodepool-functional/post.yaml
|
||||
timeout: 5400
|
||||
required-projects:
|
||||
- openstack-infra/devstack-gate
|
||||
- openstack-infra/nodepool
|
||||
|
||||
- job:
|
||||
name: nodepool-functional-src
|
||||
parent: legacy-dsvm-base
|
||||
run: playbooks/nodepool-functional-src/run.yaml
|
||||
post-run: playbooks/nodepool-functional-src/post.yaml
|
||||
timeout: 5400
|
||||
required-projects:
|
||||
- openstack-infra/devstack-gate
|
||||
- openstack-infra/glean
|
||||
- openstack-infra/nodepool
|
||||
- openstack-infra/shade
|
||||
- openstack/diskimage-builder
|
||||
|
||||
- job:
|
||||
name: nodepool-functional-py35
|
||||
parent: legacy-dsvm-base
|
||||
@ -44,16 +21,23 @@
|
||||
- openstack-infra/shade
|
||||
- openstack/diskimage-builder
|
||||
|
||||
- job:
|
||||
name: nodepool-zuul-functional
|
||||
parent: legacy-base
|
||||
run: playbooks/nodepool-zuul-functional/run.yaml
|
||||
post-run: playbooks/nodepool-zuul-functional/post.yaml
|
||||
timeout: 1800
|
||||
required-projects:
|
||||
- openstack-infra/nodepool
|
||||
- openstack-infra/zuul
|
||||
|
||||
- project:
|
||||
name: openstack-infra/nodepool
|
||||
check:
|
||||
jobs:
|
||||
- tox-docs
|
||||
- tox-cover
|
||||
- tox-pep8
|
||||
- tox-py27
|
||||
- nodepool-functional:
|
||||
voting: false
|
||||
- nodepool-functional-src:
|
||||
voting: false
|
||||
- tox-py35
|
||||
- nodepool-functional-py35:
|
||||
voting: false
|
||||
- nodepool-functional-py35-src:
|
||||
@ -61,7 +45,7 @@
|
||||
gate:
|
||||
jobs:
|
||||
- tox-pep8
|
||||
- tox-py27
|
||||
post:
|
||||
- tox-py35
|
||||
experimental:
|
||||
jobs:
|
||||
- publish-openstack-python-branch-tarball
|
||||
- nodepool-zuul-functional
|
||||
|
31
README.rst
31
README.rst
@ -47,29 +47,6 @@ If the cloud being used has no default_floating_pool defined in nova.conf,
|
||||
you will need to define a pool name using the nodepool yaml file to use
|
||||
floating ips.
|
||||
|
||||
|
||||
Set up database for interactive testing:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
mysql -u root
|
||||
|
||||
mysql> create database nodepool;
|
||||
mysql> GRANT ALL ON nodepool.* TO 'nodepool'@'localhost';
|
||||
mysql> flush privileges;
|
||||
|
||||
Set up database for unit tests:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
mysql -u root
|
||||
mysql> grant all privileges on *.* to 'openstack_citest'@'localhost' identified by 'openstack_citest' with grant option;
|
||||
mysql> flush privileges;
|
||||
mysql> create database openstack_citest;
|
||||
|
||||
Note that the script tools/test-setup.sh can be used for the step
|
||||
above.
|
||||
|
||||
Export variable for your ssh key so you can log into the created instances:
|
||||
|
||||
.. code-block:: bash
|
||||
@ -83,7 +60,7 @@ to contain your data):
|
||||
|
||||
export STATSD_HOST=127.0.0.1
|
||||
export STATSD_PORT=8125
|
||||
nodepoold -d -c tools/fake.yaml
|
||||
nodepool-launcher -d -c tools/fake.yaml
|
||||
|
||||
All logging ends up in stdout.
|
||||
|
||||
@ -92,9 +69,3 @@ Use the following tool to check on progress:
|
||||
.. code-block:: bash
|
||||
|
||||
nodepool image-list
|
||||
|
||||
After each run (the fake nova provider is only in-memory):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
mysql> delete from snapshot_image; delete from node;
|
||||
|
@ -1,8 +1,8 @@
|
||||
# This is a cross-platform list tracking distribution packages needed by tests;
|
||||
# see http://docs.openstack.org/infra/bindep/ for additional information.
|
||||
|
||||
mysql-client [test]
|
||||
mysql-server [test]
|
||||
libffi-devel [platform:rpm]
|
||||
libffi-dev [platform:dpkg]
|
||||
python-dev [platform:dpkg test]
|
||||
python-devel [platform:rpm test]
|
||||
zookeeperd [platform:dpkg test]
|
||||
|
@ -3,6 +3,3 @@ kpartx
|
||||
debootstrap
|
||||
yum-utils
|
||||
zookeeperd
|
||||
zypper
|
||||
# workarond for https://bugs.launchpad.net/ubuntu/+source/zypper/+bug/1639428
|
||||
gnupg2
|
||||
|
@ -14,8 +14,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
NODEPOOL_KEY=$HOME/.ssh/id_nodepool
|
||||
NODEPOOL_KEY_NAME=root
|
||||
NODEPOOL_PUBKEY=$HOME/.ssh/id_nodepool.pub
|
||||
NODEPOOL_INSTALL=$HOME/nodepool-venv
|
||||
NODEPOOL_CACHE_GET_PIP=/opt/stack/cache/files/get-pip.py
|
||||
@ -34,7 +32,7 @@ function install_shade {
|
||||
# BUT - install shade into a virtualenv so that we don't have issues
|
||||
# with OpenStack constraints affecting the shade dependency install.
|
||||
# This particularly shows up with os-client-config
|
||||
$NODEPOOL_INSTALL/bin/pip install -e $DEST/shade
|
||||
$NODEPOOL_INSTALL/bin/pip install $DEST/shade
|
||||
fi
|
||||
}
|
||||
|
||||
@ -45,7 +43,7 @@ function install_diskimage_builder {
|
||||
GITBRANCH["diskimage-builder"]=$DISKIMAGE_BUILDER_REPO_REF
|
||||
git_clone_by_name "diskimage-builder"
|
||||
setup_dev_lib "diskimage-builder"
|
||||
$NODEPOOL_INSTALL/bin/pip install -e $DEST/diskimage-builder
|
||||
$NODEPOOL_INSTALL/bin/pip install $DEST/diskimage-builder
|
||||
fi
|
||||
}
|
||||
|
||||
@ -56,38 +54,30 @@ function install_glean {
|
||||
GITBRANCH["glean"]=$GLEAN_REPO_REF
|
||||
git_clone_by_name "glean"
|
||||
setup_dev_lib "glean"
|
||||
$NODEPOOL_INSTALL/bin/pip install -e $DEST/glean
|
||||
$NODEPOOL_INSTALL/bin/pip install $DEST/glean
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# Install nodepool code
|
||||
function install_nodepool {
|
||||
virtualenv $NODEPOOL_INSTALL
|
||||
if python3_enabled; then
|
||||
VENV="virtualenv -p python${PYTHON3_VERSION}"
|
||||
else
|
||||
VENV="virtualenv -p python${PYTHON2_VERSION}"
|
||||
fi
|
||||
$VENV $NODEPOOL_INSTALL
|
||||
install_shade
|
||||
install_diskimage_builder
|
||||
install_glean
|
||||
|
||||
setup_develop $DEST/nodepool
|
||||
$NODEPOOL_INSTALL/bin/pip install -e $DEST/nodepool
|
||||
$NODEPOOL_INSTALL/bin/pip install $DEST/nodepool
|
||||
}
|
||||
|
||||
# requires some globals from devstack, which *might* not be stable api
|
||||
# points. If things break, investigate changes in those globals first.
|
||||
|
||||
function nodepool_create_keypairs {
|
||||
if [[ ! -f $NODEPOOL_KEY ]]; then
|
||||
ssh-keygen -f $NODEPOOL_KEY -P ""
|
||||
fi
|
||||
|
||||
cat > /tmp/ssh_wrapper <<EOF
|
||||
#!/bin/bash -ex
|
||||
sudo -H -u stack ssh -o StrictHostKeyChecking=no -i $NODEPOOL_KEY root@\$@
|
||||
|
||||
EOF
|
||||
sudo chmod 0755 /tmp/ssh_wrapper
|
||||
}
|
||||
|
||||
function nodepool_write_elements {
|
||||
sudo mkdir -p $(dirname $NODEPOOL_CONFIG)/elements/nodepool-setup/install.d
|
||||
sudo mkdir -p $(dirname $NODEPOOL_CONFIG)/elements/nodepool-setup/root.d
|
||||
@ -118,7 +108,6 @@ EOF
|
||||
function nodepool_write_config {
|
||||
sudo mkdir -p $(dirname $NODEPOOL_CONFIG)
|
||||
sudo mkdir -p $(dirname $NODEPOOL_SECURE)
|
||||
local dburi=$(database_connection_url nodepool)
|
||||
|
||||
cat > /tmp/logging.conf <<EOF
|
||||
[formatters]
|
||||
@ -178,12 +167,7 @@ EOF
|
||||
sudo mv /tmp/logging.conf $NODEPOOL_LOGGING
|
||||
|
||||
cat > /tmp/secure.conf << EOF
|
||||
[database]
|
||||
# The mysql password here may be different depending on your
|
||||
# devstack install, you should double check it (the devstack var
|
||||
# is MYSQL_PASSWORD and if unset devstack should prompt you for
|
||||
# the value).
|
||||
dburi: $dburi
|
||||
# Empty
|
||||
EOF
|
||||
sudo mv /tmp/secure.conf $NODEPOOL_SECURE
|
||||
|
||||
@ -197,131 +181,129 @@ EOF
|
||||
if [ -f $NODEPOOL_CACHE_GET_PIP ] ; then
|
||||
DIB_GET_PIP="DIB_REPOLOCATION_pip_and_virtualenv: file://$NODEPOOL_CACHE_GET_PIP"
|
||||
fi
|
||||
if [ -f /etc/ci/mirror_info.sh ] ; then
|
||||
source /etc/ci/mirror_info.sh
|
||||
if [ -f /etc/nodepool/provider ] ; then
|
||||
source /etc/nodepool/provider
|
||||
|
||||
NODEPOOL_MIRROR_HOST=${NODEPOOL_MIRROR_HOST:-mirror.$NODEPOOL_REGION.$NODEPOOL_CLOUD.openstack.org}
|
||||
NODEPOOL_MIRROR_HOST=$(echo $NODEPOOL_MIRROR_HOST|tr '[:upper:]' '[:lower:]')
|
||||
|
||||
NODEPOOL_CENTOS_MIRROR=${NODEPOOL_CENTOS_MIRROR:-http://$NODEPOOL_MIRROR_HOST/centos}
|
||||
NODEPOOL_DEBIAN_MIRROR=${NODEPOOL_DEBIAN_MIRROR:-http://$NODEPOOL_MIRROR_HOST/debian}
|
||||
NODEPOOL_UBUNTU_MIRROR=${NODEPOOL_UBUNTU_MIRROR:-http://$NODEPOOL_MIRROR_HOST/ubuntu}
|
||||
|
||||
DIB_DISTRIBUTION_MIRROR_CENTOS="DIB_DISTRIBUTION_MIRROR: $NODEPOOL_CENTOS_MIRROR"
|
||||
DIB_DISTRIBUTION_MIRROR_DEBIAN="DIB_DISTRIBUTION_MIRROR: $NODEPOOL_DEBIAN_MIRROR"
|
||||
DIB_DISTRIBUTION_MIRROR_FEDORA="DIB_DISTRIBUTION_MIRROR: $NODEPOOL_FEDORA_MIRROR"
|
||||
DIB_DISTRIBUTION_MIRROR_UBUNTU="DIB_DISTRIBUTION_MIRROR: $NODEPOOL_UBUNTU_MIRROR"
|
||||
DIB_DEBOOTSTRAP_EXTRA_ARGS="DIB_DEBOOTSTRAP_EXTRA_ARGS: '--no-check-gpg'"
|
||||
fi
|
||||
|
||||
NODEPOOL_CENTOS_7_MIN_READY=1
|
||||
NODEPOOL_DEBIAN_JESSIE_MIN_READY=1
|
||||
# TODO(pabelanger): Remove fedora-25 after fedora-26 is online
|
||||
NODEPOOL_FEDORA_25_MIN_READY=1
|
||||
NODEPOOL_FEDORA_26_MIN_READY=1
|
||||
NODEPOOL_UBUNTU_TRUSTY_MIN_READY=1
|
||||
NODEPOOL_UBUNTU_XENIAL_MIN_READY=1
|
||||
|
||||
if $NODEPOOL_PAUSE_CENTOS_7_DIB ; then
|
||||
NODEPOOL_CENTOS_7_MIN_READY=0
|
||||
fi
|
||||
if $NODEPOOL_PAUSE_DEBIAN_JESSIE_DIB ; then
|
||||
NODEPOOL_DEBIAN_JESSIE_MIN_READY=0
|
||||
fi
|
||||
if $NODEPOOL_PAUSE_FEDORA_25_DIB ; then
|
||||
NODEPOOL_FEDORA_25_MIN_READY=0
|
||||
fi
|
||||
if $NODEPOOL_PAUSE_FEDORA_26_DIB ; then
|
||||
NODEPOOL_FEDORA_26_MIN_READY=0
|
||||
fi
|
||||
if $NODEPOOL_PAUSE_UBUNTU_TRUSTY_DIB ; then
|
||||
NODEPOOL_UBUNTU_TRUSTY_MIN_READY=0
|
||||
fi
|
||||
if $NODEPOOL_PAUSE_UBUNTU_XENIAL_DIB ; then
|
||||
NODEPOOL_UBUNTU_XENIAL_MIN_READY=0
|
||||
fi
|
||||
|
||||
cat > /tmp/nodepool.yaml <<EOF
|
||||
# You will need to make and populate this path as necessary,
|
||||
# cloning nodepool does not do this. Further in this doc we have an
|
||||
# example element.
|
||||
elements-dir: $(dirname $NODEPOOL_CONFIG)/elements
|
||||
images-dir: $NODEPOOL_DIB_BASE_PATH/images
|
||||
# The mysql password here may be different depending on your
|
||||
# devstack install, you should double check it (the devstack var
|
||||
# is MYSQL_PASSWORD and if unset devstack should prompt you for
|
||||
# the value).
|
||||
dburi: '$dburi'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: localhost
|
||||
port: 2181
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: 8991
|
||||
zmq-publishers: []
|
||||
# Need to have at least one target for node allocations, but
|
||||
# this does not need to be a jenkins target.
|
||||
targets:
|
||||
- name: dummy
|
||||
assign-via-gearman: True
|
||||
|
||||
cron:
|
||||
cleanup: '*/1 * * * *'
|
||||
check: '*/15 * * * *'
|
||||
|
||||
labels:
|
||||
- name: centos-7
|
||||
image: centos-7
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: devstack
|
||||
min-ready: $NODEPOOL_CENTOS_7_MIN_READY
|
||||
- name: debian-jessie
|
||||
image: debian-jessie
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: devstack
|
||||
min-ready: $NODEPOOL_DEBIAN_JESSIE_MIN_READY
|
||||
- name: fedora-25
|
||||
min-ready: $NODEPOOL_FEDORA_25_MIN_READY
|
||||
- name: fedora-26
|
||||
image: fedora-26
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: devstack
|
||||
- name: opensuse-423
|
||||
image: opensuse-423
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: devstack
|
||||
min-ready: $NODEPOOL_FEDORA_26_MIN_READY
|
||||
- name: ubuntu-trusty
|
||||
image: ubuntu-trusty
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: devstack
|
||||
min-ready: $NODEPOOL_UBUNTU_TRUSTY_MIN_READY
|
||||
- name: ubuntu-xenial
|
||||
image: ubuntu-xenial
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: devstack
|
||||
min-ready: $NODEPOOL_UBUNTU_XENIAL_MIN_READY
|
||||
|
||||
providers:
|
||||
- name: devstack
|
||||
region-name: '$REGION_NAME'
|
||||
cloud: devstack
|
||||
api-timeout: 60
|
||||
# Long boot timeout to deal with potentially nested virt.
|
||||
boot-timeout: 600
|
||||
launch-timeout: 900
|
||||
max-servers: 5
|
||||
rate: 0.25
|
||||
images:
|
||||
diskimages:
|
||||
- name: centos-7
|
||||
min-ram: 1024
|
||||
name-filter: 'nodepool'
|
||||
username: devuser
|
||||
private-key: $NODEPOOL_KEY
|
||||
config-drive: true
|
||||
key-name: $NODEPOOL_KEY_NAME
|
||||
- name: debian-jessie
|
||||
min-ram: 512
|
||||
name-filter: 'nodepool'
|
||||
username: devuser
|
||||
private-key: $NODEPOOL_KEY
|
||||
config-drive: true
|
||||
key-name: $NODEPOOL_KEY_NAME
|
||||
- name: fedora-25
|
||||
config-drive: true
|
||||
- name: fedora-26
|
||||
min-ram: 1024
|
||||
name-filter: 'nodepool'
|
||||
username: devuser
|
||||
private-key: $NODEPOOL_KEY
|
||||
config-drive: true
|
||||
key-name: $NODEPOOL_KEY_NAME
|
||||
- name: opensuse-423
|
||||
min-ram: 1024
|
||||
name-filter: 'nodepool'
|
||||
username: devuser
|
||||
private-key: $NODEPOOL_KEY
|
||||
config-drive: true
|
||||
key-name: $NODEPOOL_KEY_NAME
|
||||
- name: ubuntu-trusty
|
||||
min-ram: 512
|
||||
name-filter: 'nodepool'
|
||||
username: devuser
|
||||
private-key: $NODEPOOL_KEY
|
||||
config-drive: true
|
||||
key-name: $NODEPOOL_KEY_NAME
|
||||
- name: ubuntu-xenial
|
||||
min-ram: 512
|
||||
name-filter: 'nodepool'
|
||||
username: devuser
|
||||
private-key: $NODEPOOL_KEY
|
||||
config-drive: true
|
||||
key-name: $NODEPOOL_KEY_NAME
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 5
|
||||
labels:
|
||||
- name: centos-7
|
||||
diskimage: centos-7
|
||||
min-ram: 1024
|
||||
flavor-name: 'nodepool'
|
||||
console-log: True
|
||||
- name: debian-jessie
|
||||
diskimage: debian-jessie
|
||||
min-ram: 512
|
||||
flavor-name: 'nodepool'
|
||||
console-log: True
|
||||
- name: fedora-25
|
||||
diskimage: fedora-25
|
||||
min-ram: 1024
|
||||
flavor-name: 'nodepool'
|
||||
console-log: True
|
||||
- name: fedora-26
|
||||
diskimage: fedora-26
|
||||
min-ram: 1024
|
||||
flavor-name: 'nodepool'
|
||||
console-log: True
|
||||
- name: ubuntu-trusty
|
||||
diskimage: ubuntu-trusty
|
||||
min-ram: 512
|
||||
flavor-name: 'nodepool'
|
||||
console-log: True
|
||||
- name: ubuntu-xenial
|
||||
diskimage: ubuntu-xenial
|
||||
min-ram: 512
|
||||
flavor-name: 'nodepool'
|
||||
console-log: True
|
||||
|
||||
diskimages:
|
||||
- name: centos-7
|
||||
@ -369,6 +351,26 @@ diskimages:
|
||||
$DIB_GLEAN_INSTALLTYPE
|
||||
$DIB_GLEAN_REPOLOCATION
|
||||
$DIB_GLEAN_REPOREF
|
||||
- name: fedora-25
|
||||
pause: $NODEPOOL_PAUSE_FEDORA_25_DIB
|
||||
rebuild-age: 86400
|
||||
elements:
|
||||
- fedora-minimal
|
||||
- vm
|
||||
- simple-init
|
||||
- devuser
|
||||
- openssh-server
|
||||
- nodepool-setup
|
||||
release: 25
|
||||
env-vars:
|
||||
TMPDIR: $NODEPOOL_DIB_BASE_PATH/tmp
|
||||
DIB_CHECKSUM: '1'
|
||||
DIB_IMAGE_CACHE: $NODEPOOL_DIB_BASE_PATH/cache
|
||||
DIB_DEV_USER_AUTHORIZED_KEYS: $NODEPOOL_PUBKEY
|
||||
$DIB_GET_PIP
|
||||
$DIB_GLEAN_INSTALLTYPE
|
||||
$DIB_GLEAN_REPOLOCATION
|
||||
$DIB_GLEAN_REPOREF
|
||||
- name: fedora-26
|
||||
pause: $NODEPOOL_PAUSE_FEDORA_26_DIB
|
||||
rebuild-age: 86400
|
||||
@ -380,27 +382,6 @@ diskimages:
|
||||
- openssh-server
|
||||
- nodepool-setup
|
||||
release: 26
|
||||
env-vars:
|
||||
TMPDIR: $NODEPOOL_DIB_BASE_PATH/tmp
|
||||
DIB_CHECKSUM: '1'
|
||||
DIB_IMAGE_CACHE: $NODEPOOL_DIB_BASE_PATH/cache
|
||||
DIB_DEV_USER_AUTHORIZED_KEYS: $NODEPOOL_PUBKEY
|
||||
$DIB_DISTRIBUTION_MIRROR_FEDORA
|
||||
$DIB_GET_PIP
|
||||
$DIB_GLEAN_INSTALLTYPE
|
||||
$DIB_GLEAN_REPOLOCATION
|
||||
$DIB_GLEAN_REPOREF
|
||||
- name: opensuse-423
|
||||
pause: $NODEPOOL_PAUSE_OPENSUSE_423_DIB
|
||||
rebuild-age: 86400
|
||||
elements:
|
||||
- opensuse-minimal
|
||||
- vm
|
||||
- simple-init
|
||||
- devuser
|
||||
- openssh-server
|
||||
- nodepool-setup
|
||||
release: 42.3
|
||||
env-vars:
|
||||
TMPDIR: $NODEPOOL_DIB_BASE_PATH/tmp
|
||||
DIB_CHECKSUM: '1'
|
||||
@ -474,27 +455,22 @@ cache:
|
||||
floating-ip: 5
|
||||
server: 5
|
||||
port: 5
|
||||
# TODO(pabelanger): Remove once glean fully supports IPv6.
|
||||
client:
|
||||
force_ipv4: True
|
||||
EOF
|
||||
sudo mv /tmp/clouds.yaml /etc/openstack/clouds.yaml
|
||||
mkdir -p $HOME/.cache/openstack/
|
||||
}
|
||||
|
||||
# Initialize database
|
||||
# Create configs
|
||||
# Setup custom flavor
|
||||
function configure_nodepool {
|
||||
# build a dedicated keypair for nodepool to use with guests
|
||||
nodepool_create_keypairs
|
||||
|
||||
# write the nodepool config
|
||||
nodepool_write_config
|
||||
|
||||
# write the elements
|
||||
nodepool_write_elements
|
||||
|
||||
# builds a fresh db
|
||||
recreate_database nodepool
|
||||
|
||||
}
|
||||
|
||||
function start_nodepool {
|
||||
@ -513,24 +489,19 @@ function start_nodepool {
|
||||
openstack --os-project-name demo --os-username demo security group rule create --ingress --protocol tcp --dst-port 1:65535 --remote-ip 0.0.0.0/0 default
|
||||
|
||||
openstack --os-project-name demo --os-username demo security group rule create --ingress --protocol udp --dst-port 1:65535 --remote-ip 0.0.0.0/0 default
|
||||
|
||||
fi
|
||||
|
||||
# create root keypair to use with glean for devstack cloud.
|
||||
nova --os-project-name demo --os-username demo \
|
||||
keypair-add --pub-key $NODEPOOL_PUBKEY $NODEPOOL_KEY_NAME
|
||||
|
||||
export PATH=$NODEPOOL_INSTALL/bin:$PATH
|
||||
|
||||
# start gearman server
|
||||
run_process geard "$NODEPOOL_INSTALL/bin/geard -p 8991 -d"
|
||||
|
||||
# run a fake statsd so we test stats sending paths
|
||||
export STATSD_HOST=localhost
|
||||
export STATSD_PORT=8125
|
||||
run_process statsd "/usr/bin/socat -u udp-recv:$STATSD_PORT -"
|
||||
|
||||
run_process nodepool "$NODEPOOL_INSTALL/bin/nodepoold -c $NODEPOOL_CONFIG -s $NODEPOOL_SECURE -l $NODEPOOL_LOGGING -d"
|
||||
# Ensure our configuration is valid.
|
||||
$NODEPOOL_INSTALL/bin/nodepool -c $NODEPOOL_CONFIG config-validate
|
||||
|
||||
run_process nodepool-launcher "$NODEPOOL_INSTALL/bin/nodepool-launcher -c $NODEPOOL_CONFIG -s $NODEPOOL_SECURE -l $NODEPOOL_LOGGING -d"
|
||||
run_process nodepool-builder "$NODEPOOL_INSTALL/bin/nodepool-builder -c $NODEPOOL_CONFIG -l $NODEPOOL_LOGGING -d"
|
||||
:
|
||||
}
|
||||
@ -545,7 +516,7 @@ function cleanup_nodepool {
|
||||
}
|
||||
|
||||
# check for service enabled
|
||||
if is_service_enabled nodepool; then
|
||||
if is_service_enabled nodepool-launcher; then
|
||||
|
||||
if [[ "$1" == "stack" && "$2" == "install" ]]; then
|
||||
# Perform installation of service source
|
||||
|
@ -8,8 +8,9 @@ NODEPOOL_DIB_BASE_PATH=/opt/dib
|
||||
# change the defaults.
|
||||
NODEPOOL_PAUSE_CENTOS_7_DIB=${NODEPOOL_PAUSE_CENTOS_7_DIB:-true}
|
||||
NODEPOOL_PAUSE_DEBIAN_JESSIE_DIB=${NODEPOOL_PAUSE_DEBIAN_JESSIE_DIB:-true}
|
||||
NODEPOOL_PAUSE_FEDORA_25_DIB=${NODEPOOL_PAUSE_FEDORA_25_DIB:-true}
|
||||
NODEPOOL_PAUSE_FEDORA_26_DIB=${NODEPOOL_PAUSE_FEDORA_26_DIB:-true}
|
||||
NODEPOOL_PAUSE_OPENSUSE_423_DIB=${NODEPOOL_PAUSE_OPENSUSE_423_DIB:-true}
|
||||
NODEPOOL_PAUSE_UBUNTU_PRECISE_DIB=${NODEPOOL_PAUSE_UBUNTU_PRECISE_DIB:-true}
|
||||
NODEPOOL_PAUSE_UBUNTU_TRUSTY_DIB=${NODEPOOL_PAUSE_UBUNTU_TRUSTY_DIB:-false}
|
||||
NODEPOOL_PAUSE_UBUNTU_XENIAL_DIB=${NODEPOOL_PAUSE_UBUNTU_XENIAL_DIB:-true}
|
||||
|
||||
@ -24,5 +25,5 @@ GLEAN_REPO_REF=${GLEAN_REPO_REF:-master}
|
||||
|
||||
enable_service geard
|
||||
enable_service statsd
|
||||
enable_service nodepool
|
||||
enable_service nodepool-launcher
|
||||
enable_service nodepool-builder
|
||||
|
@ -3,62 +3,11 @@
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Nodepool reads its secure configuration from ``/etc/nodepool/secure.conf``
|
||||
by default. The secure file is a standard ini config file, with
|
||||
one section for database, and another section for the jenkins
|
||||
secrets for each target::
|
||||
|
||||
[database]
|
||||
dburi={dburi}
|
||||
|
||||
[jenkins "{target_name}"]
|
||||
user={user}
|
||||
apikey={apikey}
|
||||
credentials={credentials}
|
||||
url={url}
|
||||
|
||||
Following settings are available::
|
||||
|
||||
**required**
|
||||
|
||||
``dburi``
|
||||
Indicates the URI for the database connection. See the `SQLAlchemy
|
||||
documentation
|
||||
<http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls>`_
|
||||
for the syntax. Example::
|
||||
|
||||
dburi='mysql+pymysql://nodepool@localhost/nodepool'
|
||||
|
||||
**optional**
|
||||
|
||||
While it is possible to run Nodepool without any Jenkins targets,
|
||||
if Jenkins is used, the `target_name` and `url` are required. The
|
||||
`user`, `apikey` and `credentials` also may be needed depending on
|
||||
the Jenkins security settings.
|
||||
|
||||
``target_name``
|
||||
Name of the jenkins target. It needs to match with a target
|
||||
specified in nodepool.yaml, in order to retrieve its settings.
|
||||
|
||||
``url``
|
||||
Url to the Jenkins REST API.
|
||||
|
||||
``user``
|
||||
Jenkins username.
|
||||
|
||||
``apikey``
|
||||
API key generated by Jenkins (not the user password).
|
||||
|
||||
``credentials``
|
||||
If provided, Nodepool will configure the Jenkins slave to use the Jenkins
|
||||
credential identified by that ID, otherwise it will use the username and
|
||||
ssh keys configured in the image.
|
||||
|
||||
Nodepool reads its configuration from ``/etc/nodepool/nodepool.yaml``
|
||||
by default. The configuration file follows the standard YAML syntax
|
||||
with a number of sections defined with top level keys. For example, a
|
||||
full configuration file may have the ``diskimages``, ``labels``,
|
||||
``providers``, and ``targets`` sections::
|
||||
and ``providers`` sections::
|
||||
|
||||
diskimages:
|
||||
...
|
||||
@ -66,12 +15,29 @@ full configuration file may have the ``diskimages``, ``labels``,
|
||||
...
|
||||
providers:
|
||||
...
|
||||
targets:
|
||||
...
|
||||
|
||||
.. note:: The builder daemon creates a UUID to uniquely identify itself and
|
||||
to mark image builds in ZooKeeper that it owns. This file will be
|
||||
named ``builder_id.txt`` and will live in the directory named by the
|
||||
:ref:`images-dir` option. If this file does not exist, it will be
|
||||
created on builder startup and a UUID will be created automatically.
|
||||
|
||||
The following sections are available. All are required unless
|
||||
otherwise indicated.
|
||||
|
||||
.. _webapp-conf:
|
||||
|
||||
webapp
|
||||
------
|
||||
|
||||
Define the webapp endpoint port and listen address.
|
||||
|
||||
Example::
|
||||
|
||||
webapp:
|
||||
port: 8005
|
||||
listen_address: '0.0.0.0'
|
||||
|
||||
.. _elements-dir:
|
||||
|
||||
elements-dir
|
||||
@ -86,6 +52,8 @@ Example::
|
||||
|
||||
elements-dir: /path/to/elements/dir
|
||||
|
||||
.. _images-dir:
|
||||
|
||||
images-dir
|
||||
----------
|
||||
|
||||
@ -97,44 +65,6 @@ Example::
|
||||
|
||||
images-dir: /path/to/images/dir
|
||||
|
||||
cron
|
||||
----
|
||||
This section is optional.
|
||||
|
||||
Nodepool runs several periodic tasks. The ``cleanup`` task deletes
|
||||
old images and servers which may have encountered errors during their
|
||||
initial deletion. The ``check`` task attempts to log into each node
|
||||
that is waiting to be used to make sure that it is still operational.
|
||||
The following illustrates how to change the schedule for these tasks
|
||||
and also indicates their default values::
|
||||
|
||||
cron:
|
||||
cleanup: '27 */6 * * *'
|
||||
check: '*/15 * * * *'
|
||||
|
||||
zmq-publishers
|
||||
--------------
|
||||
Lists the ZeroMQ endpoints for the Jenkins masters. Nodepool uses
|
||||
this to receive real-time notification that jobs are running on nodes
|
||||
or are complete and nodes may be deleted. Example::
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://jenkins1.example.com:8888
|
||||
- tcp://jenkins2.example.com:8888
|
||||
|
||||
gearman-servers
|
||||
---------------
|
||||
Lists the Zuul Gearman servers that should be consulted for real-time
|
||||
demand. Nodepool will use information from these servers to determine
|
||||
if additional nodes should be created to satisfy current demand.
|
||||
Example::
|
||||
|
||||
gearman-servers:
|
||||
- host: zuul.example.com
|
||||
port: 4730
|
||||
|
||||
The ``port`` key is optional (default: 4730).
|
||||
|
||||
zookeeper-servers
|
||||
-----------------
|
||||
Lists the ZooKeeper servers uses for coordinating information between
|
||||
@ -155,83 +85,54 @@ the supplied root path, is also optional and has no default.
|
||||
labels
|
||||
------
|
||||
|
||||
Defines the types of nodes that should be created. Maps node types to
|
||||
the images that are used to back them and the providers that are used
|
||||
to supply them. Jobs should be written to run on nodes of a certain
|
||||
label (so targets such as Jenkins don't need to know about what
|
||||
providers or images are used to create them). Example::
|
||||
Defines the types of nodes that should be created. Jobs should be
|
||||
written to run on nodes of a certain label. Example::
|
||||
|
||||
labels:
|
||||
- name: my-precise
|
||||
image: precise
|
||||
max-ready-age: 3600
|
||||
min-ready: 2
|
||||
providers:
|
||||
- name: provider1
|
||||
- name: provider2
|
||||
- name: multi-precise
|
||||
image: precise
|
||||
subnodes: 2
|
||||
min-ready: 2
|
||||
ready-script: setup_multinode.sh
|
||||
providers:
|
||||
- name: provider1
|
||||
|
||||
**required**
|
||||
|
||||
``name``
|
||||
Unique name used to tie jobs to those instances.
|
||||
|
||||
``image``
|
||||
Refers to providers images, see :ref:`images`.
|
||||
|
||||
``providers`` (list)
|
||||
Required if any nodes should actually be created (e.g., the label is not
|
||||
currently disabled, see ``min-ready`` below).
|
||||
|
||||
**optional**
|
||||
|
||||
``max-ready-age`` (int)
|
||||
Maximum number of seconds the node shall be in ready state. If
|
||||
this is exceeded the node will be deleted. A value of 0 disables this.
|
||||
Defaults to 0.
|
||||
|
||||
``min-ready`` (default: 2)
|
||||
Minimum instances that should be in a ready state. Set to -1 to have the
|
||||
label considered disabled. ``min-ready`` is best-effort based on available
|
||||
capacity and is not a guaranteed allocation.
|
||||
|
||||
``subnodes``
|
||||
Used to configure multi-node support. If a `subnodes` key is supplied to
|
||||
an image, it indicates that the specified number of additional nodes of the
|
||||
same image type should be created and associated with each node for that
|
||||
image.
|
||||
|
||||
Only one node from each such group will be added to the target, the
|
||||
subnodes are expected to communicate directly with each other. In the
|
||||
example above, for each Precise node added to the target system, two
|
||||
additional nodes will be created and associated with it.
|
||||
|
||||
``ready-script``
|
||||
A script to be used to perform any last minute changes to a node after it
|
||||
has been launched but before it is put in the READY state to receive jobs.
|
||||
For more information, see :ref:`scripts`.
|
||||
|
||||
.. _diskimages:
|
||||
|
||||
diskimages
|
||||
----------
|
||||
|
||||
This section lists the images to be built using diskimage-builder. The
|
||||
name of the diskimage is mapped to the :ref:`images` section of the
|
||||
provider, to determine which providers should received uploads of each
|
||||
name of the diskimage is mapped to the :ref:`provider_diskimages` section
|
||||
of the provider, to determine which providers should received uploads of each
|
||||
image. The diskimage will be built in every format required by the
|
||||
providers with which it is associated. Because Nodepool needs to know
|
||||
which formats to build, if the diskimage will only be built if it
|
||||
appears in at least one provider.
|
||||
|
||||
To remove a diskimage from the system entirely, remove all associated
|
||||
entries in :ref:`images` and remove its entry from `diskimages`. All
|
||||
uploads will be deleted as well as the files on disk.
|
||||
entries in :ref:`provider_diskimages` and remove its entry from `diskimages`.
|
||||
All uploads will be deleted as well as the files on disk.
|
||||
|
||||
Example configuration::
|
||||
|
||||
diskimages:
|
||||
- name: precise
|
||||
- name: ubuntu-precise
|
||||
pause: False
|
||||
rebuild-age: 86400
|
||||
elements:
|
||||
@ -245,6 +146,7 @@ Example configuration::
|
||||
- growroot
|
||||
- infra-package-needs
|
||||
release: precise
|
||||
username: zuul
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_CHECKSUM: '1'
|
||||
@ -252,7 +154,7 @@ Example configuration::
|
||||
DIB_APT_LOCAL_CACHE: '0'
|
||||
DIB_DISABLE_APT_CLEANUP: '1'
|
||||
FS_TYPE: ext3
|
||||
- name: xenial
|
||||
- name: ubuntu-xenial
|
||||
pause: True
|
||||
rebuild-age: 86400
|
||||
formats:
|
||||
@ -269,6 +171,7 @@ Example configuration::
|
||||
- growroot
|
||||
- infra-package-needs
|
||||
release: precise
|
||||
username: ubuntu
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_CHECKSUM: '1'
|
||||
@ -281,7 +184,8 @@ Example configuration::
|
||||
**required**
|
||||
|
||||
``name``
|
||||
Identifier to reference the disk image in :ref:`images` and :ref:`labels`.
|
||||
Identifier to reference the disk image in :ref:`provider_diskimages`
|
||||
and :ref:`labels`.
|
||||
|
||||
**optional**
|
||||
|
||||
@ -312,124 +216,124 @@ Example configuration::
|
||||
``pause`` (bool)
|
||||
When set to True, nodepool-builder will not build the diskimage.
|
||||
|
||||
``username`` (string)
|
||||
The username that a consumer should use when connecting onto the node. Defaults
|
||||
to ``zuul``.
|
||||
|
||||
.. _provider:
|
||||
|
||||
provider
|
||||
providers
|
||||
---------
|
||||
|
||||
Lists the OpenStack cloud providers Nodepool should use. Within each
|
||||
provider, the Nodepool image types are also defined (see
|
||||
:ref:`images` for details). Example::
|
||||
|
||||
providers:
|
||||
- name: provider1
|
||||
cloud: example
|
||||
region-name: 'region1'
|
||||
max-servers: 96
|
||||
rate: 1.0
|
||||
availability-zones:
|
||||
- az1
|
||||
boot-timeout: 120
|
||||
launch-timeout: 900
|
||||
template-hostname: 'template-{image.name}-{timestamp}'
|
||||
ipv6-preferred: False
|
||||
networks:
|
||||
- name: 'some-network-name'
|
||||
images:
|
||||
- name: trusty
|
||||
min-ram: 8192
|
||||
name-filter: 'something to match'
|
||||
username: jenkins
|
||||
user-home: '/home/jenkins'
|
||||
private-key: /var/lib/jenkins/.ssh/id_rsa
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
- name: precise
|
||||
min-ram: 8192
|
||||
username: jenkins
|
||||
user-home: '/home/jenkins'
|
||||
private-key: /var/lib/jenkins/.ssh/id_rsa
|
||||
- name: devstack-trusty
|
||||
min-ram: 30720
|
||||
username: jenkins
|
||||
private-key: /home/nodepool/.ssh/id_rsa
|
||||
- name: provider2
|
||||
username: 'username'
|
||||
password: 'password'
|
||||
auth-url: 'http://auth.provider2.example.com/'
|
||||
project-name: 'project'
|
||||
service-type: 'compute'
|
||||
service-name: 'compute'
|
||||
region-name: 'region1'
|
||||
max-servers: 96
|
||||
rate: 1.0
|
||||
template-hostname: '{image.name}-{timestamp}-nodepool-template'
|
||||
images:
|
||||
- name: precise
|
||||
min-ram: 8192
|
||||
username: jenkins
|
||||
user-home: '/home/jenkins'
|
||||
private-key: /var/lib/jenkins/.ssh/id_rsa
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
|
||||
**cloud configuration***
|
||||
|
||||
**preferred**
|
||||
|
||||
``cloud``
|
||||
There are two methods supported for configuring cloud entries. The preferred
|
||||
method is to create an ``~/.config/openstack/clouds.yaml`` file containing
|
||||
your cloud configuration information. Then, use ``cloud`` to refer to a
|
||||
named entry in that file.
|
||||
|
||||
More information about the contents of `clouds.yaml` can be found in
|
||||
`the os-client-config documentation <http://docs.openstack.org/developer/os-client-config/>`_.
|
||||
|
||||
**compatablity**
|
||||
|
||||
For backwards compatibility reasons, you can also include
|
||||
portions of the cloud configuration directly in ``nodepool.yaml``. Not all
|
||||
of the options settable via ``clouds.yaml`` are available.
|
||||
|
||||
``username``
|
||||
|
||||
``password``
|
||||
|
||||
``project-id`` OR ``project-name``
|
||||
Some clouds may refer to the ``project-id`` as ``tenant-id``.
|
||||
Some clouds may refer to the ``project-name`` as ``tenant-name``.
|
||||
|
||||
``auth-url``
|
||||
Keystone URL.
|
||||
|
||||
``image-type``
|
||||
Specifies the image type supported by this provider. The disk images built
|
||||
by diskimage-builder will output an image for each ``image-type`` specified
|
||||
by a provider using that particular diskimage.
|
||||
|
||||
By default, ``image-type`` is set to the value returned from
|
||||
``os-client-config`` and can be omitted in most cases.
|
||||
Lists the providers Nodepool should use. Each provider is associated to
|
||||
a driver listed below.
|
||||
|
||||
**required**
|
||||
|
||||
``name``
|
||||
|
||||
``max-servers``
|
||||
Maximum number of servers spawnable on this provider.
|
||||
|
||||
**optional**
|
||||
|
||||
``availability-zones`` (list)
|
||||
Without it nodepool will rely on nova to schedule an availability zone.
|
||||
``driver``
|
||||
Default to *openstack*
|
||||
|
||||
If it is provided the value should be a list of availability zone names.
|
||||
Nodepool will select one at random and provide that to nova. This should
|
||||
give a good distribution of availability zones being used. If you need more
|
||||
control of the distribution you can use multiple logical providers each
|
||||
providing a different list of availabiltiy zones.
|
||||
``max-concurrency``
|
||||
Maximum number of node requests that this provider is allowed to handle
|
||||
concurrently. The default, if not specified, is to have no maximum. Since
|
||||
each node request is handled by a separate thread, this can be useful for
|
||||
limiting the number of threads used by the nodepool-launcher daemon.
|
||||
|
||||
|
||||
OpenStack driver
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
Within each OpenStack provider the available Nodepool image types are defined
|
||||
(see :ref:`provider_diskimages`).
|
||||
|
||||
An OpenStack provider's resources are partitioned into groups called "pools"
|
||||
(see :ref:`pools` for details), and within a pool, the node types which are
|
||||
to be made available are listed (see :ref:`pool_labels` for
|
||||
details).
|
||||
|
||||
Example::
|
||||
|
||||
providers:
|
||||
- name: provider1
|
||||
driver: openstack
|
||||
cloud: example
|
||||
region-name: 'region1'
|
||||
rate: 1.0
|
||||
boot-timeout: 120
|
||||
launch-timeout: 900
|
||||
launch-retries: 3
|
||||
image-name-format: '{image_name}-{timestamp}'
|
||||
hostname-format: '{label.name}-{provider.name}-{node.id}'
|
||||
diskimages:
|
||||
- name: trusty
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
- name: precise
|
||||
- name: devstack-trusty
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
availability-zones:
|
||||
- az1
|
||||
networks:
|
||||
- some-network-name
|
||||
labels:
|
||||
- name: trusty
|
||||
min-ram: 8192
|
||||
diskimage: trusty
|
||||
console-log: True
|
||||
- name: precise
|
||||
min-ram: 8192
|
||||
diskimage: precise
|
||||
- name: devstack-trusty
|
||||
min-ram: 8192
|
||||
diskimage: devstack-trusty
|
||||
- name: provider2
|
||||
driver: openstack
|
||||
cloud: example2
|
||||
region-name: 'region1'
|
||||
rate: 1.0
|
||||
image-name-format: '{image_name}-{timestamp}'
|
||||
hostname-format: '{label.name}-{provider.name}-{node.id}'
|
||||
diskimages:
|
||||
- name: precise
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: trusty
|
||||
min-ram: 8192
|
||||
diskimage: trusty
|
||||
- name: precise
|
||||
min-ram: 8192
|
||||
diskimage: precise
|
||||
- name: devstack-trusty
|
||||
min-ram: 8192
|
||||
diskimage: devstack-trusty
|
||||
|
||||
**required**
|
||||
|
||||
``cloud``
|
||||
Name of a cloud configured in ``clouds.yaml``.
|
||||
|
||||
The instances spawned by nodepool will inherit the default security group
|
||||
of the project specified in the cloud definition in `clouds.yaml`. This means
|
||||
that when working with Zuul, for example, SSH traffic (TCP/22) must be allowed
|
||||
in the project's default security group for Zuul to be able to reach instances.
|
||||
|
||||
More information about the contents of `clouds.yaml` can be found in
|
||||
`the os-client-config documentation <http://docs.openstack.org/developer/os-client-config/>`_.
|
||||
|
||||
**optional**
|
||||
|
||||
``boot-timeout``
|
||||
Once an instance is active, how long to try connecting to the
|
||||
@ -454,31 +358,22 @@ provider, the Nodepool image types are also defined (see
|
||||
|
||||
Default None
|
||||
|
||||
``networks`` (dict)
|
||||
Specify custom Neutron networks that get attached to each
|
||||
node. Specify the ``name`` of the network (a string).
|
||||
``launch-retries``
|
||||
|
||||
``ipv6-preferred``
|
||||
If it is set to True, nodepool will try to find ipv6 in public net first
|
||||
as the ip address for ssh connection to build snapshot images and create
|
||||
jenkins slave definition. If ipv6 is not found or the key is not
|
||||
specified or set to False, ipv4 address will be used.
|
||||
The number of times to retry launching a server before considering the job
|
||||
failed.
|
||||
|
||||
``api-timeout`` (compatability)
|
||||
Timeout for the OpenStack API calls client in seconds. Prefer setting
|
||||
this in `clouds.yaml`
|
||||
|
||||
``service-type`` (compatability)
|
||||
Prefer setting this in `clouds.yaml`.
|
||||
|
||||
``service-name`` (compatability)
|
||||
Prefer setting this in `clouds.yaml`.
|
||||
Default 3.
|
||||
|
||||
``region-name``
|
||||
|
||||
``template-hostname``
|
||||
``hostname-format``
|
||||
Hostname template to use for the spawned instance.
|
||||
Default ``template-{image.name}-{timestamp}``
|
||||
Default ``{label.name}-{provider.name}-{node.id}``
|
||||
|
||||
``image-name-format``
|
||||
Format for image names that are uploaded to providers.
|
||||
Default ``{image_name}-{timestamp}``
|
||||
|
||||
``rate``
|
||||
In seconds, amount to wait between operations on the provider.
|
||||
@ -489,12 +384,88 @@ provider, the Nodepool image types are also defined (see
|
||||
OpenStack project and will attempt to clean unattached floating ips that
|
||||
may have leaked around restarts.
|
||||
|
||||
.. _images:
|
||||
.. _pools:
|
||||
|
||||
images
|
||||
~~~~~~
|
||||
pools
|
||||
~~~~~
|
||||
|
||||
Each entry in a provider's `images` section must correspond to an
|
||||
A pool defines a group of resources from an OpenStack provider. Each pool has a
|
||||
maximum number of nodes which can be launched from it, along with a
|
||||
number of cloud-related attributes used when launching nodes.
|
||||
|
||||
Example::
|
||||
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
availability-zones:
|
||||
- az1
|
||||
networks:
|
||||
- some-network-name
|
||||
auto-floating-ip: False
|
||||
labels:
|
||||
- name: trusty
|
||||
min-ram: 8192
|
||||
diskimage: trusty
|
||||
console-log: True
|
||||
- name: precise
|
||||
min-ram: 8192
|
||||
diskimage: precise
|
||||
- name: devstack-trusty
|
||||
min-ram: 8192
|
||||
diskimage: devstack-trusty
|
||||
|
||||
**required**
|
||||
|
||||
``name``
|
||||
|
||||
|
||||
**optional**
|
||||
|
||||
``max-cores``
|
||||
Maximum number of cores usable from this pool. This can be used to limit
|
||||
usage of the tenant. If not defined nodepool can use all cores up to the
|
||||
quota of the tenant.
|
||||
|
||||
``max-servers``
|
||||
Maximum number of servers spawnable from this pool. This can be used to
|
||||
limit the number of servers. If not defined nodepool can create as many
|
||||
servers the tenant allows.
|
||||
|
||||
``max-ram``
|
||||
Maximum ram usable from this pool. This can be used to limit the amount of
|
||||
ram allocated by nodepool. If not defined nodepool can use as much ram as
|
||||
the tenant allows.
|
||||
|
||||
``availability-zones`` (list)
|
||||
A list of availability zones to use.
|
||||
|
||||
If this setting is omitted, nodepool will fetch the list of all
|
||||
availability zones from nova. To restrict nodepool to a subset
|
||||
of availability zones, supply a list of availability zone names
|
||||
in this setting.
|
||||
|
||||
Nodepool chooses an availability zone from the list at random
|
||||
when creating nodes but ensures that all nodes for a given
|
||||
request are placed in the same availability zone.
|
||||
|
||||
``networks`` (list)
|
||||
Specify custom Neutron networks that get attached to each
|
||||
node. Specify the name or id of the network as a string.
|
||||
|
||||
``auto-floating-ip`` (bool)
|
||||
Specify custom behavior of allocating floating ip for each node.
|
||||
When set to False, nodepool-launcher will not apply floating ip
|
||||
for nodes. When zuul instances and nodes are deployed in the same
|
||||
internal private network, set the option to False to save floating ip
|
||||
for cloud provider. The default value is True.
|
||||
|
||||
.. _provider_diskimages:
|
||||
|
||||
diskimages
|
||||
~~~~~~~~~~
|
||||
|
||||
Each entry in a provider's `diskimages` section must correspond to an
|
||||
entry in :ref:`diskimages`. Such an entry indicates that the
|
||||
corresponding diskimage should be uploaded for use in this provider.
|
||||
Additionally, any nodes that are created using the uploaded image will
|
||||
@ -505,16 +476,14 @@ images will be deleted from the provider.
|
||||
|
||||
Example configuration::
|
||||
|
||||
images:
|
||||
diskimages:
|
||||
- name: precise
|
||||
pause: False
|
||||
min-ram: 8192
|
||||
name-filter: 'something to match'
|
||||
username: jenkins
|
||||
private-key: /var/lib/jenkins/.ssh/id_rsa
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
- name: windows
|
||||
connection-type: winrm
|
||||
|
||||
**required**
|
||||
|
||||
@ -522,86 +491,143 @@ Example configuration::
|
||||
Identifier to refer this image from :ref:`labels` and :ref:`diskimages`
|
||||
sections.
|
||||
|
||||
``min-ram``
|
||||
Determine the flavor to use (e.g. ``m1.medium``, ``m1.large``,
|
||||
etc). The smallest flavor that meets the ``min-ram`` requirements
|
||||
will be chosen. To further filter by flavor name, see optional
|
||||
``name-filter`` below.
|
||||
|
||||
**optional**
|
||||
|
||||
``name-filter``
|
||||
Additional filter complementing ``min-ram``, will be required to match on
|
||||
the flavor-name (e.g. Rackspace offer a "Performance" flavour; setting
|
||||
`name-filter` to ``Performance`` will ensure the chosen flavor also
|
||||
contains this string as well as meeting `min-ram` requirements).
|
||||
|
||||
``pause`` (bool)
|
||||
When set to True, nodepool-builder will not upload the image to the
|
||||
provider.
|
||||
|
||||
``username``
|
||||
Nodepool expects that user to exist after running the script indicated by
|
||||
``setup``. Default ``jenkins``
|
||||
|
||||
``key-name``
|
||||
If provided, named keypair in nova that will be provided to server create.
|
||||
|
||||
``private-key``
|
||||
Default ``/var/lib/jenkins/.ssh/id_rsa``
|
||||
|
||||
``config-drive`` (boolean)
|
||||
Whether config drive should be used for the image. Default ``True``
|
||||
Whether config drive should be used for the image. Defaults to unset which
|
||||
will use the cloud's default behavior.
|
||||
|
||||
``meta`` (dict)
|
||||
Arbitrary key/value metadata to store for this server using the Nova
|
||||
metadata service. A maximum of five entries is allowed, and both keys and
|
||||
values must be 255 characters or less.
|
||||
|
||||
.. _targets:
|
||||
``connection-type`` (string)
|
||||
The connection type that a consumer should use when connecting onto the
|
||||
node. For most diskimages this is not necessary. However when creating
|
||||
Windows images this could be 'winrm' to enable access via ansible.
|
||||
|
||||
targets
|
||||
-------
|
||||
|
||||
Lists the Jenkins masters to which Nodepool should attach nodes after
|
||||
they are created. Nodes of each label will be evenly distributed
|
||||
across all of the targets which are on-line::
|
||||
.. _provider_cloud_images:
|
||||
|
||||
targets:
|
||||
- name: jenkins1
|
||||
hostname: '{label.name}-{provider.name}-{node_id}'
|
||||
subnode-hostname: '{label.name}-{provider.name}-{node_id}-{subnode_id}'
|
||||
- name: jenkins2
|
||||
hostname: '{label.name}-{provider.name}-{node_id}'
|
||||
subnode-hostname: '{label.name}-{provider.name}-{node_id}-{subnode_id}'
|
||||
cloud-images
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Each cloud-image entry in :ref:`labels` refers to an entry in this section.
|
||||
This is a way for modifying launch parameters of the nodes (currently only
|
||||
config-drive).
|
||||
|
||||
Example configuration::
|
||||
|
||||
cloud-images:
|
||||
- name: trusty-external
|
||||
config-drive: False
|
||||
- name: windows-external
|
||||
connection-type: winrm
|
||||
|
||||
**required**
|
||||
|
||||
``name``
|
||||
Identifier for the system an instance is attached to.
|
||||
Identifier to refer this cloud-image from :ref:`labels` section.
|
||||
Since this name appears elsewhere in the nodepool configuration
|
||||
file, you may want to use your own descriptive name here and use
|
||||
one of ``image-id`` or ``image-name`` to specify the cloud image
|
||||
so that if the image name or id changes on the cloud, the impact
|
||||
to your Nodepool configuration will be minimal. However, if
|
||||
neither of those attributes are provided, this is also assumed to
|
||||
be the image name or ID in the cloud.
|
||||
|
||||
**optional**
|
||||
|
||||
``hostname``
|
||||
Default ``{label.name}-{provider.name}-{node_id}``
|
||||
``config-drive`` (boolean)
|
||||
Whether config drive should be used for the cloud image. Defaults to
|
||||
unset which will use the cloud's default behavior.
|
||||
|
||||
``subnode-hostname``
|
||||
Default ``{label.name}-{provider.name}-{node_id}-{subnode_id}``
|
||||
``image-id`` (str)
|
||||
If this is provided, it is used to select the image from the cloud
|
||||
provider by ID, rather than name. Mutually exclusive with ``image-name``.
|
||||
|
||||
``rate``
|
||||
In seconds. Default 1.0
|
||||
``image-name`` (str)
|
||||
If this is provided, it is used to select the image from the cloud
|
||||
provider by this name or ID. Mutually exclusive with ``image-id``.
|
||||
|
||||
``jenkins`` (dict)
|
||||
``username`` (str)
|
||||
The username that a consumer should use when connecting onto the node.
|
||||
|
||||
``test-job`` (optional)
|
||||
Setting this would cause a newly created instance to be in a TEST state.
|
||||
The job name given will then be executed with the node name as a
|
||||
parameter.
|
||||
``connection-type`` (str)
|
||||
The connection type that a consumer should use when connecting onto the
|
||||
node. For most diskimages this is not necessary. However when creating
|
||||
Windows images this could be 'winrm' to enable access via ansible.
|
||||
|
||||
If the job succeeds, move the node into READY state and relabel it with
|
||||
the appropriate label (from the image name).
|
||||
.. _pool_labels:
|
||||
|
||||
If it fails, immediately delete the node.
|
||||
labels
|
||||
~~~~~~
|
||||
|
||||
If the job never runs, the node will eventually be cleaned up by the
|
||||
periodic cleanup task.
|
||||
Each entry in a pool`s `labels` section indicates that the
|
||||
corresponding label is available for use in this pool. When creating
|
||||
nodes for a label, the flavor-related attributes in that label's
|
||||
section will be used.
|
||||
|
||||
Example configuration::
|
||||
|
||||
labels:
|
||||
- name: precise
|
||||
min-ram: 8192
|
||||
flavor-name: 'something to match'
|
||||
console-log: True
|
||||
|
||||
**required**
|
||||
|
||||
``name``
|
||||
Identifier to refer this image from :ref:`labels` and :ref:`diskimages`
|
||||
sections.
|
||||
|
||||
**one of**
|
||||
|
||||
``diskimage``
|
||||
Refers to provider's diskimages, see :ref:`provider_diskimages`.
|
||||
|
||||
``cloud-image``
|
||||
Refers to the name of an externally managed image in the cloud that already
|
||||
exists on the provider. The value of ``cloud-image`` should match the
|
||||
``name`` of a previously configured entry from the ``cloud-images`` section
|
||||
of the provider. See :ref:`provider_cloud_images`.
|
||||
|
||||
**at least one of**
|
||||
|
||||
``flavor-name``
|
||||
Name or id of the flavor to use. If ``min-ram`` is omitted, it
|
||||
must be an exact match. If ``min-ram`` is given, ``flavor-name`` will
|
||||
be used to find flavor names that meet ``min-ram`` and also contain
|
||||
``flavor-name``.
|
||||
|
||||
``min-ram``
|
||||
Determine the flavor to use (e.g. ``m1.medium``, ``m1.large``,
|
||||
etc). The smallest flavor that meets the ``min-ram`` requirements
|
||||
will be chosen.
|
||||
|
||||
**optional**
|
||||
|
||||
``boot-from-volume`` (bool)
|
||||
If given, the label for use in this pool will create a volume from the
|
||||
image and boot the node from it.
|
||||
|
||||
Default: False
|
||||
|
||||
``key-name``
|
||||
If given, is the name of a keypair that will be used when booting each
|
||||
server.
|
||||
|
||||
``console-log`` (default: False)
|
||||
On the failure of the ssh ready check, download the server console log to
|
||||
aid in debuging the problem.
|
||||
|
||||
``volume-size``
|
||||
When booting an image from volume, how big should the created volume be.
|
||||
|
||||
In gigabytes. Default 50.
|
||||
|
@ -4,7 +4,7 @@ Nodepool
|
||||
Nodepool is a system for launching single-use test nodes on demand
|
||||
based on images built with cached data. It is designed to work with
|
||||
any OpenStack based cloud, and is part of a suite of tools that form a
|
||||
comprehensive test system including Jenkins and Zuul.
|
||||
comprehensive test system, including Zuul.
|
||||
|
||||
Contents:
|
||||
|
||||
@ -13,7 +13,6 @@ Contents:
|
||||
|
||||
installation
|
||||
configuration
|
||||
scripts
|
||||
operation
|
||||
devguide
|
||||
|
||||
@ -21,5 +20,6 @@ Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
|
@ -3,51 +3,12 @@
|
||||
Installation
|
||||
============
|
||||
|
||||
Nodepool consists of a set of long-running daemons which use an SQL
|
||||
database, a ZooKeeper cluster, and communicates with Jenkins using
|
||||
ZeroMQ.
|
||||
Nodepool consists of a long-running daemon which uses ZooKeeper
|
||||
for coordination with Zuul.
|
||||
|
||||
External Requirements
|
||||
---------------------
|
||||
|
||||
Jenkins
|
||||
~~~~~~~
|
||||
|
||||
You should have a Jenkins server running with the `ZMQ Event Publisher
|
||||
<http://git.openstack.org/cgit/openstack-infra/zmq-event-publisher/tree/README>`_
|
||||
plugin installed (it is available in the Jenkins Update Center). Be
|
||||
sure that the machine where you plan to run Nodepool can connect to
|
||||
the ZMQ port specified by the plugin on your Jenkins master(s).
|
||||
|
||||
Zuul
|
||||
~~~~
|
||||
|
||||
If you plan to use Nodepool with Zuul (it is optional), you should
|
||||
ensure that Nodepool can connect to the gearman port on your Zuul
|
||||
server (TCP 4730 by default). This will allow Nodepool to respond to
|
||||
current Zuul demand. If you elect not to connect Nodepool to Zuul, it
|
||||
will still operate in a node-replacement mode.
|
||||
|
||||
Database
|
||||
~~~~~~~~
|
||||
|
||||
Nodepool requires an SQL server. MySQL with the InnoDB storage engine
|
||||
is tested and recommended. PostgreSQL should work fine. Due to the
|
||||
high number of concurrent connections from Nodepool, SQLite is not
|
||||
recommended. When adding or deleting nodes, Nodepool will hold open a
|
||||
database connection for each node. Be sure to configure the database
|
||||
server to support at least a number of connections equal to twice the
|
||||
number of nodes you expect to be in use at once.
|
||||
|
||||
All that is necessary is that the database is created. Nodepool will
|
||||
handle the schema by itself when it is run.
|
||||
|
||||
MySQL Example::
|
||||
|
||||
CREATE USER 'nodepool'@'localhost' IDENTIFIED BY '<password>';
|
||||
CREATE DATABASE nodepooldb;
|
||||
GRANT ALL ON nodepooldb.* TO 'nodepool'@'localhost';
|
||||
|
||||
ZooKeeper
|
||||
~~~~~~~~~
|
||||
|
||||
@ -88,22 +49,28 @@ Or install directly from a git checkout with::
|
||||
|
||||
pip install .
|
||||
|
||||
Note that some distributions provide a libzmq1 which does not support
|
||||
RCVTIMEO. Removing this libzmq1 from the system libraries will ensure
|
||||
pip compiles a libzmq1 with appropriate options for the version of
|
||||
pyzmq used by nodepool.
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
Nodepool has two required configuration files: secure.conf and
|
||||
nodepool.yaml, and an optional logging configuration file logging.conf.
|
||||
The secure.conf file is used to store nodepool configurations that contain
|
||||
sensitive data, such as the Nodepool database password and Jenkins
|
||||
api key. The nodepool.yaml files is used to store all other
|
||||
configurations.
|
||||
|
||||
The logging configuration file is in the standard python logging
|
||||
`configuration file format
|
||||
<http://docs.python.org/2/library/logging.config.html#configuration-file-format>`_.
|
||||
Nodepool has one required configuration file, which defaults to
|
||||
``/etc/nodepool/nodepool.yaml``. This can be changed with the ``-c`` option.
|
||||
The Nodepool configuration file is described in :ref:`configuration`.
|
||||
|
||||
There is support for a secure file that is used to store nodepool
|
||||
configurations that contain sensitive data. It currently only supports
|
||||
specifying ZooKeeper credentials. If ZooKeeper credentials are defined in
|
||||
both configuration files, the data in the secure file takes precedence.
|
||||
The secure file location can be changed with the ``-s`` option and follows
|
||||
the same file format as the Nodepool configuration file.
|
||||
|
||||
There is an optional logging configuration file, specified with the ``-l``
|
||||
option. The logging configuration file can accept either:
|
||||
|
||||
* the traditional ini python logging `configuration file format
|
||||
<https://docs.python.org/2/library/logging.config.html#configuration-file-format>`_.
|
||||
|
||||
* a `.yml` or `.yaml` suffixed file that will be parsed and loaded as the newer
|
||||
`dictConfig format
|
||||
<https://docs.python.org/2/library/logging.config.html#configuration-dictionary-schema>`_.
|
||||
|
||||
The Nodepool configuration file is described in :ref:`configuration`.
|
||||
|
@ -5,13 +5,17 @@ Operation
|
||||
|
||||
Nodepool has two components which run as daemons. The
|
||||
``nodepool-builder`` daemon is responsible for building diskimages and
|
||||
uploading them to providers, and the ``nodepoold`` daemon is
|
||||
uploading them to providers, and the ``nodepool-launcher`` daemon is
|
||||
responsible for launching and deleting nodes.
|
||||
|
||||
Both daemons frequently re-read their configuration file after
|
||||
starting to support adding or removing new images and providers, or
|
||||
otherwise altering the configuration.
|
||||
|
||||
These daemons communicate with each other via a Zookeeper database.
|
||||
You must run Zookeeper and at least one of each of these daemons to
|
||||
have a functioning Nodepool installation.
|
||||
|
||||
Nodepool-builder
|
||||
----------------
|
||||
|
||||
@ -31,14 +35,14 @@ safe, it is recommended to run a single instance of
|
||||
only a single build thread (the default).
|
||||
|
||||
|
||||
Nodepoold
|
||||
---------
|
||||
Nodepool-launcher
|
||||
-----------------
|
||||
|
||||
The main nodepool daemon is named ``nodepoold`` and is responsible for
|
||||
launching instances from the images created and uploaded by
|
||||
``nodepool-builder``.
|
||||
The main nodepool daemon is named ``nodepool-launcher`` and is
|
||||
responsible for managing cloud instances launched from the images
|
||||
created and uploaded by ``nodepool-builder``.
|
||||
|
||||
When a new image is created and uploaded, ``nodepoold`` will
|
||||
When a new image is created and uploaded, ``nodepool-launcher`` will
|
||||
immediately start using it when launching nodes (Nodepool always uses
|
||||
the most recent image for a given provider in the ``ready`` state).
|
||||
Nodepool will delete images if they are not the most recent or second
|
||||
@ -51,9 +55,9 @@ using the previous image.
|
||||
Daemon usage
|
||||
------------
|
||||
|
||||
To start the main Nodepool daemon, run **nodepoold**:
|
||||
To start the main Nodepool daemon, run **nodepool-launcher**:
|
||||
|
||||
.. program-output:: nodepoold --help
|
||||
.. program-output:: nodepool-launcher --help
|
||||
:nostderr:
|
||||
|
||||
To start the nodepool-builder daemon, run **nodepool--builder**:
|
||||
@ -77,21 +81,73 @@ When Nodepool creates instances, it will assign the following nova
|
||||
metadata:
|
||||
|
||||
groups
|
||||
A json-encoded list containing the name of the image and the name
|
||||
A comma separated list containing the name of the image and the name
|
||||
of the provider. This may be used by the Ansible OpenStack
|
||||
inventory plugin.
|
||||
|
||||
nodepool
|
||||
A json-encoded dictionary with the following entries:
|
||||
nodepool_image_name
|
||||
The name of the image as a string.
|
||||
|
||||
image_name
|
||||
The name of the image as a string.
|
||||
nodepool_provider_name
|
||||
The name of the provider as a string.
|
||||
|
||||
provider_name
|
||||
The name of the provider as a string.
|
||||
nodepool_node_id
|
||||
The nodepool id of the node as an integer.
|
||||
|
||||
node_id
|
||||
The nodepool id of the node as an integer.
|
||||
Common Management Tasks
|
||||
-----------------------
|
||||
|
||||
In the course of running a Nodepool service you will find that there are
|
||||
some common operations that will be performed. Like the services
|
||||
themselves these are split into two groups, image management and
|
||||
instance management.
|
||||
|
||||
Image Management
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Before Nodepool can launch any cloud instances it must have images to boot
|
||||
off of. ``nodepool dib-image-list`` will show you which images are available
|
||||
locally on disk. These images on disk are then uploaded to clouds,
|
||||
``nodepool image-list`` will show you what images are bootable in your
|
||||
various clouds.
|
||||
|
||||
If you need to force a new image to be built to pick up a new feature more
|
||||
quickly than the normal rebuild cycle (which defaults to 24 hours) you can
|
||||
manually trigger a rebuild. Using ``nodepool image-build`` you can tell
|
||||
Nodepool to begin a new image build now. Note that depending on work that
|
||||
the nodepool-builder is already performing this may queue the build. Check
|
||||
``nodepool dib-image-list`` to see the current state of the builds. Once
|
||||
the image is built it is automatically uploaded to all of the clouds
|
||||
configured to use that image.
|
||||
|
||||
At times you may need to stop using an existing image because it is broken.
|
||||
Your two major options here are to build a new image to replace the existing
|
||||
image or to delete the existing image and have Nodepool fall back on using
|
||||
the previous image. Rebuilding and uploading can be slow so typically the
|
||||
best option is to simply ``nodepool image-delete`` the most recent image
|
||||
which will cause Nodepool to fallback on using the previous image. Howevever,
|
||||
if you do this without "pausing" the image it will be immediately reuploaded.
|
||||
You will want to pause the image if you need to further investigate why
|
||||
the image is not being built correctly. If you know the image will be built
|
||||
correctly you can simple delete the built image and remove it from all clouds
|
||||
which will cause it to be rebuilt using ``nodepool dib-image-delete``.
|
||||
|
||||
Instance Management
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
With working images in providers you should see Nodepool launching instances
|
||||
in these providers using the images it built. You may find that you need to
|
||||
debug a particular job failure manually. An easy way to do this is to
|
||||
``nodepool hold`` an instance then log in to the instance and perform any
|
||||
necessary debugging steps. Note that this doesn't stop the job running there,
|
||||
what it will do is prevent Nodepool from automatically deleting this instance
|
||||
once the job is complete.
|
||||
|
||||
In some circumstances like manually holding an instance above, or wanting to
|
||||
force a job restart you may want to delete a running instance. You can issue
|
||||
a ``nodepool delete`` to force nodepool to do this.
|
||||
|
||||
Complete command help info is below.
|
||||
|
||||
Command Line Tools
|
||||
------------------
|
||||
@ -151,38 +207,11 @@ If Nodepool's database gets out of sync with reality, the following
|
||||
commands can help identify compute instances or images that are
|
||||
unknown to Nodepool:
|
||||
|
||||
alien-list
|
||||
^^^^^^^^^^
|
||||
.. program-output:: nodepool alien-list --help
|
||||
:nostderr:
|
||||
|
||||
alien-image-list
|
||||
^^^^^^^^^^^^^^^^
|
||||
.. program-output:: nodepool alien-image-list --help
|
||||
:nostderr:
|
||||
|
||||
In the case that a job is randomly failing for an unknown cause, it
|
||||
may be necessary to instruct nodepool to automatically hold a node on
|
||||
which that job has failed. To do so, use the ``job-create``
|
||||
command to specify the job name and how many failed nodes should be
|
||||
held. When debugging is complete, use ''job-delete'' to disable the
|
||||
feature.
|
||||
|
||||
job-create
|
||||
^^^^^^^^^^
|
||||
.. program-output:: nodepool job-create --help
|
||||
:nostderr:
|
||||
|
||||
job-list
|
||||
^^^^^^^^
|
||||
.. program-output:: nodepool job-list --help
|
||||
:nostderr:
|
||||
|
||||
job-delete
|
||||
^^^^^^^^^^
|
||||
.. program-output:: nodepool job-delete --help
|
||||
:nostderr:
|
||||
|
||||
Removing a Provider
|
||||
-------------------
|
||||
|
||||
|
@ -1,45 +0,0 @@
|
||||
.. _scripts:
|
||||
|
||||
Node Ready Scripts
|
||||
==================
|
||||
|
||||
Each label can specify a ready script with `ready-script`. This script can be
|
||||
used to perform any last minute changes to a node after it has been launched
|
||||
but before it is put in the READY state to receive jobs. In particular, it
|
||||
can read the files in /etc/nodepool to perform multi-node related setup.
|
||||
|
||||
Those files include:
|
||||
|
||||
**/etc/nodepool/role**
|
||||
Either the string ``primary`` or ``sub`` indicating whether this
|
||||
node is the primary (the node added to the target and which will run
|
||||
the job), or a sub-node.
|
||||
**/etc/nodepool/node**
|
||||
The IP address of this node.
|
||||
**/etc/nodepool/node_private**
|
||||
The private IP address of this node.
|
||||
**/etc/nodepool/primary_node**
|
||||
The IP address of the primary node, usable for external access.
|
||||
**/etc/nodepool/primary_node_private**
|
||||
The Private IP address of the primary node, for internal communication.
|
||||
**/etc/nodepool/sub_nodes**
|
||||
The IP addresses of the sub nodes, one on each line,
|
||||
usable for external access.
|
||||
**/etc/nodepool/sub_nodes_private**
|
||||
The Private IP addresses of the sub nodes, one on each line.
|
||||
**/etc/nodepool/id_rsa**
|
||||
An OpenSSH private key generated specifically for this node group.
|
||||
**/etc/nodepool/id_rsa.pub**
|
||||
The corresponding public key.
|
||||
**/etc/nodepool/provider**
|
||||
Information about the provider in a shell-usable form. This
|
||||
includes the following information:
|
||||
|
||||
**NODEPOOL_PROVIDER**
|
||||
The name of the provider
|
||||
**NODEPOOL_CLOUD**
|
||||
The name of the cloud
|
||||
**NODEPOOL_REGION**
|
||||
The name of the region
|
||||
**NODEPOOL_AZ**
|
||||
The name of the availability zone (if available)
|
@ -1,418 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright (C) 2013 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
This module holds classes that represent concepts in nodepool's
|
||||
allocation algorithm.
|
||||
|
||||
The algorithm is:
|
||||
|
||||
Setup:
|
||||
|
||||
* Establish the node providers with their current available
|
||||
capacity.
|
||||
* Establish requests that are to be made of each provider for a
|
||||
certain label.
|
||||
* Indicate which providers can supply nodes of that label.
|
||||
* Indicate to which targets nodes of a certain label from a certain
|
||||
provider may be distributed (and the weight that should be
|
||||
given to each target when distributing).
|
||||
|
||||
Run:
|
||||
|
||||
* For each label, set the requested number of nodes from each
|
||||
provider to be proportional to that providers overall capacity.
|
||||
|
||||
* Define the 'priority' of a request as the number of requests for
|
||||
the same label from other providers.
|
||||
|
||||
* For each provider, sort the requests by the priority. This puts
|
||||
requests that can be serviced by the fewest providers first.
|
||||
|
||||
* Grant each such request in proportion to that requests portion of
|
||||
the total amount requested by requests of the same priority.
|
||||
|
||||
* The nodes allocated by a grant are then distributed to the targets
|
||||
which are associated with the provider and label, in proportion to
|
||||
that target's portion of the sum of the weights of each target for
|
||||
that label.
|
||||
"""
|
||||
|
||||
import functools
|
||||
|
||||
# History allocation tracking
|
||||
|
||||
# The goal of the history allocation tracking is to ensure forward
|
||||
# progress by not starving any particular label when in over-quota
|
||||
# situations. For example, if you have two labels, say 'fedora' and
|
||||
# 'ubuntu', and 'ubuntu' is requesting many more nodes than 'fedora',
|
||||
# it is quite possible that 'fedora' never gets any allocations. If
|
||||
# 'fedora' is required for a gate-check job, older changes may wait
|
||||
# in Zuul's pipelines longer than expected while jobs for newer
|
||||
# changes continue to receive 'ubuntu' nodes and overall merge
|
||||
# throughput decreases during such contention.
|
||||
#
|
||||
# We track the history of allocations by label. A persistent
|
||||
# AllocationHistory object should be kept and passed along with each
|
||||
# AllocationRequest, which records its initial request in the history
|
||||
# via recordRequest().
|
||||
#
|
||||
# When a sub-allocation gets a grant, it records this via a call to
|
||||
# AllocationHistory.recordGrant(). All the sub-allocations
|
||||
# contribute to tracking the total grants for the parent
|
||||
# AllocationRequest.
|
||||
#
|
||||
# When finished requesting grants from all providers,
|
||||
# AllocationHistory.grantsDone() should be called to store the
|
||||
# allocation state in the history.
|
||||
#
|
||||
# This history is used AllocationProvider.makeGrants() to prioritize
|
||||
# requests that have not been granted in prior iterations.
|
||||
# AllocationHistory.getWaitTime will return how many iterations
|
||||
# each label has been waiting for an allocation.
|
||||
|
||||
|
||||
class AllocationHistory(object):
|
||||
'''A history of allocation requests and grants'''
|
||||
|
||||
def __init__(self, history=100):
|
||||
# current allocations for this iteration
|
||||
# keeps elements of type
|
||||
# label -> (request, granted)
|
||||
self.current_allocations = {}
|
||||
|
||||
self.history = history
|
||||
# list of up to <history> previous current_allocation
|
||||
# dictionaries
|
||||
self.past_allocations = []
|
||||
|
||||
def recordRequest(self, label, amount):
|
||||
try:
|
||||
a = self.current_allocations[label]
|
||||
a['requested'] += amount
|
||||
except KeyError:
|
||||
self.current_allocations[label] = dict(requested=amount,
|
||||
allocated=0)
|
||||
|
||||
def recordGrant(self, label, amount):
|
||||
try:
|
||||
a = self.current_allocations[label]
|
||||
a['allocated'] += amount
|
||||
except KeyError:
|
||||
# granted but not requested? shouldn't happen
|
||||
raise
|
||||
|
||||
def grantsDone(self):
|
||||
# save this round of allocations/grants up to our history
|
||||
self.past_allocations.insert(0, self.current_allocations)
|
||||
self.past_allocations = self.past_allocations[:self.history]
|
||||
self.current_allocations = {}
|
||||
|
||||
def getWaitTime(self, label):
|
||||
# go through the history of allocations and calculate how many
|
||||
# previous iterations this label has received none of its
|
||||
# requested allocations.
|
||||
wait = 0
|
||||
|
||||
# We don't look at the current_alloctions here; only
|
||||
# historical. With multiple providers, possibly the first
|
||||
# provider has given nodes to the waiting label (which would
|
||||
# be recorded in current_allocations), and a second provider
|
||||
# should fall back to using the usual ratio-based mechanism?
|
||||
for i, a in enumerate(self.past_allocations):
|
||||
if (label in a) and (a[label]['allocated'] == 0):
|
||||
wait = i + 1
|
||||
continue
|
||||
|
||||
# only interested in consecutive failures to allocate.
|
||||
break
|
||||
|
||||
return wait
|
||||
|
||||
|
||||
class AllocationProvider(object):
|
||||
"""A node provider and its capacity."""
|
||||
def __init__(self, name, available):
|
||||
self.name = name
|
||||
# if this is negative, many of the calcuations turn around and
|
||||
# we start handing out nodes that don't exist.
|
||||
self.available = available if available >= 0 else 0
|
||||
self.sub_requests = []
|
||||
self.grants = []
|
||||
|
||||
def __repr__(self):
|
||||
return '<AllocationProvider %s>' % self.name
|
||||
|
||||
def makeGrants(self):
|
||||
# build a list of (request,wait-time) tuples
|
||||
all_reqs = [(x, x.getWaitTime()) for x in self.sub_requests]
|
||||
|
||||
# reqs with no wait time get processed via ratio mechanism
|
||||
reqs = [x[0] for x in all_reqs if x[1] == 0]
|
||||
|
||||
# we prioritize whoever has been waiting the longest and give
|
||||
# them whatever is available. If we run out, put them back in
|
||||
# the ratio queue
|
||||
waiters = [x for x in all_reqs if x[1] != 0]
|
||||
waiters.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
for w in waiters:
|
||||
w = w[0]
|
||||
if self.available > 0:
|
||||
w.grant(min(int(w.amount), self.available))
|
||||
else:
|
||||
reqs.append(w)
|
||||
|
||||
# Sort the remaining requests by priority so we fill the most
|
||||
# specific requests first (e.g., if this provider is the only
|
||||
# one that can supply foo nodes, then it should focus on
|
||||
# supplying them and leave bar nodes to other providers).
|
||||
reqs.sort(lambda a, b: cmp(a.getPriority(), b.getPriority()))
|
||||
|
||||
for req in reqs:
|
||||
total_requested = 0.0
|
||||
# Within a specific priority, limit the number of
|
||||
# available nodes to a value proportionate to the request.
|
||||
reqs_at_this_level = [r for r in reqs
|
||||
if r.getPriority() == req.getPriority()]
|
||||
for r in reqs_at_this_level:
|
||||
total_requested += r.amount
|
||||
if total_requested:
|
||||
ratio = float(req.amount) / total_requested
|
||||
else:
|
||||
ratio = 0.0
|
||||
|
||||
grant = int(round(req.amount))
|
||||
grant = min(grant, int(round(self.available * ratio)))
|
||||
# This adjusts our availability as well as the values of
|
||||
# other requests, so values will be correct the next time
|
||||
# through the loop.
|
||||
req.grant(grant)
|
||||
|
||||
|
||||
class AllocationRequest(object):
|
||||
"""A request for a number of labels."""
|
||||
|
||||
def __init__(self, name, amount, history=None):
|
||||
self.name = name
|
||||
self.amount = float(amount)
|
||||
# Sub-requests of individual providers that make up this
|
||||
# request. AllocationProvider -> AllocationSubRequest
|
||||
self.sub_requests = {}
|
||||
# Targets to which nodes from this request may be assigned.
|
||||
# AllocationTarget -> AllocationRequestTarget
|
||||
self.request_targets = {}
|
||||
|
||||
if history is not None:
|
||||
self.history = history
|
||||
else:
|
||||
self.history = AllocationHistory()
|
||||
|
||||
self.history.recordRequest(name, amount)
|
||||
|
||||
# subrequests use these
|
||||
self.recordGrant = functools.partial(self.history.recordGrant, name)
|
||||
self.getWaitTime = functools.partial(self.history.getWaitTime, name)
|
||||
|
||||
def __repr__(self):
|
||||
return '<AllocationRequest for %s of %s>' % (self.amount, self.name)
|
||||
|
||||
def addTarget(self, target, current):
|
||||
art = AllocationRequestTarget(self, target, current)
|
||||
self.request_targets[target] = art
|
||||
|
||||
def addProvider(self, provider, target, subnodes):
|
||||
# Handle being called multiple times with different targets.
|
||||
s = self.sub_requests.get(provider)
|
||||
if not s:
|
||||
s = AllocationSubRequest(self, provider, subnodes)
|
||||
agt = s.addTarget(self.request_targets[target])
|
||||
self.sub_requests[provider] = s
|
||||
if s not in provider.sub_requests:
|
||||
provider.sub_requests.append(s)
|
||||
self.makeRequests()
|
||||
return s, agt
|
||||
|
||||
def makeRequests(self):
|
||||
# (Re-)distribute this request across all of its providers.
|
||||
total_available = 0.0
|
||||
for sub_request in self.sub_requests.values():
|
||||
total_available += sub_request.provider.available
|
||||
for sub_request in self.sub_requests.values():
|
||||
if total_available:
|
||||
ratio = float(sub_request.provider.available) / total_available
|
||||
else:
|
||||
ratio = 0.0
|
||||
sub_request.setAmount(ratio * self.amount)
|
||||
|
||||
|
||||
class AllocationSubRequest(object):
|
||||
"""A request for a number of images from a specific provider."""
|
||||
def __init__(self, request, provider, subnodes):
|
||||
self.request = request
|
||||
self.provider = provider
|
||||
self.amount = 0.0
|
||||
self.subnodes = subnodes
|
||||
self.targets = []
|
||||
|
||||
def __repr__(self):
|
||||
return '<AllocationSubRequest for %s (out of %s) of %s from %s>' % (
|
||||
self.amount, self.request.amount, self.request.name,
|
||||
self.provider.name)
|
||||
|
||||
def addTarget(self, request_target):
|
||||
agt = AllocationGrantTarget(self, request_target)
|
||||
self.targets.append(agt)
|
||||
return agt
|
||||
|
||||
def setAmount(self, amount):
|
||||
self.amount = amount
|
||||
|
||||
def getPriority(self):
|
||||
return len(self.request.sub_requests)
|
||||
|
||||
def getWaitTime(self):
|
||||
return self.request.getWaitTime()
|
||||
|
||||
def grant(self, amount):
|
||||
# Grant this request (with the supplied amount). Adjust this
|
||||
# sub-request's value to the actual, as well as the values of
|
||||
# any remaining sub-requests.
|
||||
|
||||
# fractional amounts don't make sense
|
||||
assert int(amount) == amount
|
||||
|
||||
# Remove from the set of sub-requests so that this is not
|
||||
# included in future calculations.
|
||||
self.provider.sub_requests.remove(self)
|
||||
del self.request.sub_requests[self.provider]
|
||||
if amount > 0:
|
||||
grant = AllocationGrant(self.request, self.provider,
|
||||
amount, self.targets)
|
||||
self.request.recordGrant(amount)
|
||||
# This is now a grant instead of a request.
|
||||
self.provider.grants.append(grant)
|
||||
else:
|
||||
grant = None
|
||||
amount = 0
|
||||
self.amount = amount
|
||||
# Adjust provider and request values accordingly.
|
||||
self.request.amount -= amount
|
||||
subnode_factor = 1 + self.subnodes
|
||||
self.provider.available -= (amount * subnode_factor)
|
||||
# Adjust the requested values for related sub-requests.
|
||||
self.request.makeRequests()
|
||||
# Allocate these granted nodes to targets.
|
||||
if grant:
|
||||
grant.makeAllocations()
|
||||
|
||||
|
||||
class AllocationGrant(object):
|
||||
"""A grant of a certain number of nodes of an image from a
|
||||
specific provider."""
|
||||
|
||||
def __init__(self, request, provider, amount, targets):
|
||||
self.request = request
|
||||
self.provider = provider
|
||||
self.amount = amount
|
||||
self.targets = targets
|
||||
|
||||
def __repr__(self):
|
||||
return '<AllocationGrant of %s of %s from %s>' % (
|
||||
self.amount, self.request.name, self.provider.name)
|
||||
|
||||
def makeAllocations(self):
|
||||
# Allocate this grant to the linked targets.
|
||||
total_current = 0
|
||||
for agt in self.targets:
|
||||
total_current += agt.request_target.current
|
||||
amount = self.amount
|
||||
# Add the nodes in this allocation to the total number of
|
||||
# nodes for this image so that we're setting our target
|
||||
# allocations based on a portion of the total future nodes.
|
||||
total_current += amount
|
||||
remaining_targets = len(self.targets)
|
||||
for agt in self.targets:
|
||||
# Evenly distribute the grants across all targets
|
||||
ratio = 1.0 / remaining_targets
|
||||
# Take the weight and apply it to the total number of
|
||||
# nodes to this image to figure out how many of the total
|
||||
# nodes should ideally be on this target.
|
||||
desired_count = int(round(ratio * total_current))
|
||||
# The number of nodes off from our calculated target.
|
||||
delta = desired_count - agt.request_target.current
|
||||
# Use the delta as the allocation for this target, but
|
||||
# make sure it's bounded by 0 and the number of nodes we
|
||||
# have available to allocate.
|
||||
allocation = min(delta, amount)
|
||||
allocation = max(allocation, 0)
|
||||
|
||||
# The next time through the loop, we have reduced our
|
||||
# grant by this amount.
|
||||
amount -= allocation
|
||||
# Don't consider this target's count in the total number
|
||||
# of nodes in the next iteration, nor the nodes we have
|
||||
# just allocated.
|
||||
total_current -= agt.request_target.current
|
||||
total_current -= allocation
|
||||
# Since we aren't considering this target's count, also
|
||||
# don't consider this target itself when calculating the
|
||||
# ratio.
|
||||
remaining_targets -= 1
|
||||
# Set the amount of this allocation.
|
||||
agt.allocate(allocation)
|
||||
|
||||
|
||||
class AllocationTarget(object):
|
||||
"""A target to which nodes may be assigned."""
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
return '<AllocationTarget %s>' % (self.name)
|
||||
|
||||
|
||||
class AllocationRequestTarget(object):
|
||||
"""A request associated with a target to which nodes may be assigned."""
|
||||
def __init__(self, request, target, current):
|
||||
self.target = target
|
||||
self.request = request
|
||||
self.current = current
|
||||
|
||||
|
||||
class AllocationGrantTarget(object):
|
||||
"""A target for a specific grant to which nodes may be assigned."""
|
||||
def __init__(self, sub_request, request_target):
|
||||
self.sub_request = sub_request
|
||||
self.request_target = request_target
|
||||
self.amount = 0
|
||||
|
||||
def __repr__(self):
|
||||
return '<AllocationGrantTarget for %s of %s to %s>' % (
|
||||
self.amount, self.sub_request.request.name,
|
||||
self.request_target.target.name)
|
||||
|
||||
def allocate(self, amount):
|
||||
# This is essentially the output of this system. This
|
||||
# represents the number of nodes of a specific image from a
|
||||
# specific provider that should be assigned to a specific
|
||||
# target.
|
||||
self.amount = amount
|
||||
# Update the number of nodes of this image that are assigned
|
||||
# to this target to assist in other allocation calculations
|
||||
self.request_target.current += amount
|
@ -21,20 +21,22 @@ import subprocess
|
||||
import threading
|
||||
import time
|
||||
import shlex
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
import config as nodepool_config
|
||||
import exceptions
|
||||
import provider_manager
|
||||
import stats
|
||||
import zk
|
||||
from nodepool import config as nodepool_config
|
||||
from nodepool import exceptions
|
||||
from nodepool import provider_manager
|
||||
from nodepool import stats
|
||||
from nodepool import zk
|
||||
|
||||
|
||||
MINS = 60
|
||||
HOURS = 60 * MINS
|
||||
IMAGE_TIMEOUT = 6 * HOURS # How long to wait for an image save
|
||||
SUSPEND_WAIT_TIME = 30 # How long to wait between checks for
|
||||
# ZooKeeper connectivity if it disappears.
|
||||
# How long to wait for an image save
|
||||
IMAGE_TIMEOUT = 6 * HOURS
|
||||
|
||||
# How long to wait between checks for ZooKeeper connectivity if it disappears.
|
||||
SUSPEND_WAIT_TIME = 30
|
||||
|
||||
# HP Cloud requires qemu compat with 0.10. That version works elsewhere,
|
||||
# so just hardcode it for all qcow2 building
|
||||
@ -108,17 +110,19 @@ class DibImageFile(object):
|
||||
|
||||
|
||||
class BaseWorker(threading.Thread):
|
||||
def __init__(self, config_path, interval, zk):
|
||||
def __init__(self, builder_id, config_path, secure_path, interval, zk):
|
||||
super(BaseWorker, self).__init__()
|
||||
self.log = logging.getLogger("nodepool.builder.BaseWorker")
|
||||
self.daemon = True
|
||||
self._running = False
|
||||
self._config = None
|
||||
self._config_path = config_path
|
||||
self._secure_path = secure_path
|
||||
self._zk = zk
|
||||
self._hostname = socket.gethostname()
|
||||
self._statsd = stats.get_client()
|
||||
self._interval = interval
|
||||
self._builder_id = builder_id
|
||||
|
||||
def _checkForZooKeeperChanges(self, new_config):
|
||||
'''
|
||||
@ -129,7 +133,7 @@ class BaseWorker(threading.Thread):
|
||||
'''
|
||||
if self._config.zookeeper_servers != new_config.zookeeper_servers:
|
||||
self.log.debug("Detected ZooKeeper server changes")
|
||||
self._zk.resetHosts(new_config.zookeeper_servers.values())
|
||||
self._zk.resetHosts(list(new_config.zookeeper_servers.values()))
|
||||
|
||||
@property
|
||||
def running(self):
|
||||
@ -145,9 +149,12 @@ class CleanupWorker(BaseWorker):
|
||||
and any local DIB builds.
|
||||
'''
|
||||
|
||||
def __init__(self, name, config_path, interval, zk):
|
||||
super(CleanupWorker, self).__init__(config_path, interval, zk)
|
||||
self.log = logging.getLogger("nodepool.builder.CleanupWorker.%s" % name)
|
||||
def __init__(self, name, builder_id, config_path, secure_path,
|
||||
interval, zk):
|
||||
super(CleanupWorker, self).__init__(builder_id, config_path,
|
||||
secure_path, interval, zk)
|
||||
self.log = logging.getLogger(
|
||||
"nodepool.builder.CleanupWorker.%s" % name)
|
||||
self.name = 'CleanupWorker.%s' % name
|
||||
|
||||
def _buildUploadRecencyTable(self):
|
||||
@ -178,7 +185,7 @@ class CleanupWorker(BaseWorker):
|
||||
)
|
||||
|
||||
# Sort uploads by state_time (upload time) and keep the 2 most recent
|
||||
for i in self._rtable.keys():
|
||||
for i in list(self._rtable.keys()):
|
||||
for p in self._rtable[i].keys():
|
||||
self._rtable[i][p].sort(key=lambda x: x[2], reverse=True)
|
||||
self._rtable[i][p] = self._rtable[i][p][:2]
|
||||
@ -222,27 +229,32 @@ class CleanupWorker(BaseWorker):
|
||||
if e.errno != 2: # No such file or directory
|
||||
raise e
|
||||
|
||||
def _deleteLocalBuild(self, image, build_id, builder):
|
||||
def _deleteLocalBuild(self, image, build):
|
||||
'''
|
||||
Remove expired image build from local disk.
|
||||
|
||||
:param str image: Name of the image whose build we are deleting.
|
||||
:param str build_id: ID of the build we want to delete.
|
||||
:param str builder: hostname of the build.
|
||||
:param ImageBuild build: The build we want to delete.
|
||||
|
||||
:returns: True if files were deleted, False if none were found.
|
||||
'''
|
||||
base = "-".join([image, build_id])
|
||||
base = "-".join([image, build.id])
|
||||
files = DibImageFile.from_image_id(self._config.imagesdir, base)
|
||||
if not files:
|
||||
# NOTE(pabelanger): It is possible we don't have any files because
|
||||
# diskimage-builder failed. So, check to see if we have the correct
|
||||
# builder so we can removed the data from zookeeper.
|
||||
if builder == self._hostname:
|
||||
|
||||
# To maintain backward compatibility with builders that didn't
|
||||
# use unique builder IDs before, but do now, always compare to
|
||||
# hostname as well since some ZK data may still reference that.
|
||||
if (build.builder_id == self._builder_id or
|
||||
build.builder == self._hostname
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
self.log.info("Doing cleanup for %s:%s" % (image, build_id))
|
||||
self.log.info("Doing cleanup for %s:%s" % (image, build.id))
|
||||
|
||||
manifest_dir = None
|
||||
|
||||
@ -251,7 +263,8 @@ class CleanupWorker(BaseWorker):
|
||||
if not manifest_dir:
|
||||
path, ext = filename.rsplit('.', 1)
|
||||
manifest_dir = path + ".d"
|
||||
map(self._removeDibItem, [filename, f.md5_file, f.sha256_file])
|
||||
items = [filename, f.md5_file, f.sha256_file]
|
||||
list(map(self._removeDibItem, items))
|
||||
|
||||
try:
|
||||
shutil.rmtree(manifest_dir)
|
||||
@ -271,8 +284,7 @@ class CleanupWorker(BaseWorker):
|
||||
self._deleteUpload(upload)
|
||||
|
||||
def _cleanupObsoleteProviderUploads(self, provider, image, build_id):
|
||||
image_names_for_provider = provider.images.keys()
|
||||
if image in image_names_for_provider:
|
||||
if image in provider.diskimages:
|
||||
# This image is in use for this provider
|
||||
return
|
||||
|
||||
@ -353,9 +365,7 @@ class CleanupWorker(BaseWorker):
|
||||
for build in builds:
|
||||
base = "-".join([image, build.id])
|
||||
files = DibImageFile.from_image_id(self._config.imagesdir, base)
|
||||
# If we have local dib files OR if our hostname matches the
|
||||
# recorded owner hostname, consider this our build.
|
||||
if files or (self._hostname == build.builder):
|
||||
if files:
|
||||
ret.append(build)
|
||||
return ret
|
||||
|
||||
@ -388,7 +398,8 @@ class CleanupWorker(BaseWorker):
|
||||
self.log.info("Removing failed upload record: %s" % upload)
|
||||
self._zk.deleteUpload(image, build_id, provider, upload.id)
|
||||
elif upload.state == zk.DELETING:
|
||||
self.log.info("Removing deleted upload and record: %s" % upload)
|
||||
self.log.info(
|
||||
"Removing deleted upload and record: %s" % upload)
|
||||
self._deleteUpload(upload)
|
||||
elif upload.state == zk.FAILED:
|
||||
self.log.info("Removing failed upload and record: %s" % upload)
|
||||
@ -403,7 +414,7 @@ class CleanupWorker(BaseWorker):
|
||||
all_builds = self._zk.getBuilds(image)
|
||||
builds_to_keep = set([b for b in sorted(all_builds, reverse=True,
|
||||
key=lambda y: y.state_time)
|
||||
if b.state==zk.READY][:2])
|
||||
if b.state == zk.READY][:2])
|
||||
local_builds = set(self._filterLocalBuilds(image, all_builds))
|
||||
diskimage = self._config.diskimages.get(image)
|
||||
if not diskimage and not local_builds:
|
||||
@ -471,7 +482,7 @@ class CleanupWorker(BaseWorker):
|
||||
self._zk.storeBuild(image, build, build.id)
|
||||
|
||||
# Release the lock here so we can delete the build znode
|
||||
if self._deleteLocalBuild(image, build.id, build.builder):
|
||||
if self._deleteLocalBuild(image, build):
|
||||
if not self._zk.deleteBuild(image, build.id):
|
||||
self.log.error("Unable to delete build %s because"
|
||||
" uploads still remain.", build)
|
||||
@ -483,9 +494,13 @@ class CleanupWorker(BaseWorker):
|
||||
self._running = True
|
||||
while self._running:
|
||||
# Don't do work if we've lost communication with the ZK cluster
|
||||
did_suspend = False
|
||||
while self._zk and (self._zk.suspended or self._zk.lost):
|
||||
did_suspend = True
|
||||
self.log.info("ZooKeeper suspended. Waiting")
|
||||
time.sleep(SUSPEND_WAIT_TIME)
|
||||
if did_suspend:
|
||||
self.log.info("ZooKeeper available. Resuming")
|
||||
|
||||
try:
|
||||
self._run()
|
||||
@ -502,6 +517,8 @@ class CleanupWorker(BaseWorker):
|
||||
Body of run method for exception handling purposes.
|
||||
'''
|
||||
new_config = nodepool_config.loadConfig(self._config_path)
|
||||
if self._secure_path:
|
||||
nodepool_config.loadSecureConfig(new_config, self._secure_path)
|
||||
if not self._config:
|
||||
self._config = new_config
|
||||
|
||||
@ -514,38 +531,14 @@ class CleanupWorker(BaseWorker):
|
||||
|
||||
|
||||
class BuildWorker(BaseWorker):
|
||||
def __init__(self, name, config_path, interval, zk, dib_cmd):
|
||||
super(BuildWorker, self).__init__(config_path, interval, zk)
|
||||
def __init__(self, name, builder_id, config_path, secure_path,
|
||||
interval, zk, dib_cmd):
|
||||
super(BuildWorker, self).__init__(builder_id, config_path, secure_path,
|
||||
interval, zk)
|
||||
self.log = logging.getLogger("nodepool.builder.BuildWorker.%s" % name)
|
||||
self.name = 'BuildWorker.%s' % name
|
||||
self.dib_cmd = dib_cmd
|
||||
|
||||
def _running_under_virtualenv(self):
|
||||
# NOTE: borrowed from pip:locations.py
|
||||
if hasattr(sys, 'real_prefix'):
|
||||
return True
|
||||
elif sys.prefix != getattr(sys, "base_prefix", sys.prefix):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _activate_virtualenv(self):
|
||||
"""Run as a pre-exec function to activate current virtualenv
|
||||
|
||||
If we are invoked directly as /path/ENV/nodepool-builer (as
|
||||
done by an init script, for example) then /path/ENV/bin will
|
||||
not be in our $PATH, meaning we can't find disk-image-create.
|
||||
Apart from that, dib also needs to run in an activated
|
||||
virtualenv so it can find utils like dib-run-parts. Run this
|
||||
before exec of dib to ensure the current virtualenv (if any)
|
||||
is activated.
|
||||
"""
|
||||
if self._running_under_virtualenv():
|
||||
activate_this = os.path.join(sys.prefix, "bin", "activate_this.py")
|
||||
if not os.path.exists(activate_this):
|
||||
raise exceptions.BuilderError("Running in a virtualenv, but "
|
||||
"cannot find: %s" % activate_this)
|
||||
execfile(activate_this, dict(__file__=activate_this))
|
||||
|
||||
def _checkForScheduledImageUpdates(self):
|
||||
'''
|
||||
Check every DIB image to see if it has aged out and needs rebuilt.
|
||||
@ -553,7 +546,7 @@ class BuildWorker(BaseWorker):
|
||||
for diskimage in self._config.diskimages.values():
|
||||
# Check if we've been told to shutdown
|
||||
# or if ZK connection is suspended
|
||||
if not self.running or self._zk.suspended or self._zk.lost:
|
||||
if not self._running or self._zk.suspended or self._zk.lost:
|
||||
return
|
||||
try:
|
||||
self._checkImageForScheduledImageUpdates(diskimage)
|
||||
@ -586,7 +579,8 @@ class BuildWorker(BaseWorker):
|
||||
if (not builds
|
||||
or (now - builds[0].state_time) >= diskimage.rebuild_age
|
||||
or not set(builds[0].formats).issuperset(diskimage.image_types)
|
||||
):
|
||||
):
|
||||
|
||||
try:
|
||||
with self._zk.imageBuildLock(diskimage.name, blocking=False):
|
||||
# To avoid locking each image repeatedly, we have an
|
||||
@ -595,7 +589,8 @@ class BuildWorker(BaseWorker):
|
||||
# lock acquisition. If it's not the same build as
|
||||
# identified in the first check above, assume another
|
||||
# BuildWorker created the build for us and continue.
|
||||
builds2 = self._zk.getMostRecentBuilds(1, diskimage.name, zk.READY)
|
||||
builds2 = self._zk.getMostRecentBuilds(
|
||||
1, diskimage.name, zk.READY)
|
||||
if builds2 and builds[0].id != builds2[0].id:
|
||||
return
|
||||
|
||||
@ -603,6 +598,7 @@ class BuildWorker(BaseWorker):
|
||||
|
||||
data = zk.ImageBuild()
|
||||
data.state = zk.BUILDING
|
||||
data.builder_id = self._builder_id
|
||||
data.builder = self._hostname
|
||||
data.formats = list(diskimage.image_types)
|
||||
|
||||
@ -620,7 +616,7 @@ class BuildWorker(BaseWorker):
|
||||
for diskimage in self._config.diskimages.values():
|
||||
# Check if we've been told to shutdown
|
||||
# or if ZK connection is suspended
|
||||
if not self.running or self._zk.suspended or self._zk.lost:
|
||||
if not self._running or self._zk.suspended or self._zk.lost:
|
||||
return
|
||||
try:
|
||||
self._checkImageForManualBuildRequest(diskimage)
|
||||
@ -653,6 +649,7 @@ class BuildWorker(BaseWorker):
|
||||
|
||||
data = zk.ImageBuild()
|
||||
data.state = zk.BUILDING
|
||||
data.builder_id = self._builder_id
|
||||
data.builder = self._hostname
|
||||
data.formats = list(diskimage.image_types)
|
||||
|
||||
@ -719,7 +716,6 @@ class BuildWorker(BaseWorker):
|
||||
shlex.split(cmd),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
preexec_fn=self._activate_virtualenv,
|
||||
env=env)
|
||||
except OSError as e:
|
||||
raise exceptions.BuilderError(
|
||||
@ -738,19 +734,26 @@ class BuildWorker(BaseWorker):
|
||||
# interrupted during the build. If so, wait for it to return.
|
||||
# It could transition directly from SUSPENDED to CONNECTED, or go
|
||||
# through the LOST state before CONNECTED.
|
||||
did_suspend = False
|
||||
while self._zk.suspended or self._zk.lost:
|
||||
did_suspend = True
|
||||
self.log.info("ZooKeeper suspended during build. Waiting")
|
||||
time.sleep(SUSPEND_WAIT_TIME)
|
||||
if did_suspend:
|
||||
self.log.info("ZooKeeper available. Resuming")
|
||||
|
||||
build_data = zk.ImageBuild()
|
||||
build_data.builder_id = self._builder_id
|
||||
build_data.builder = self._hostname
|
||||
build_data.username = diskimage.username
|
||||
|
||||
if self._zk.didLoseConnection:
|
||||
self.log.info("ZooKeeper lost while building %s" % diskimage.name)
|
||||
self._zk.resetLostFlag()
|
||||
build_data.state = zk.FAILED
|
||||
elif p.returncode:
|
||||
self.log.info("DIB failed creating %s" % diskimage.name)
|
||||
self.log.info(
|
||||
"DIB failed creating %s (%s)" % (diskimage.name, p.returncode))
|
||||
build_data.state = zk.FAILED
|
||||
else:
|
||||
self.log.info("DIB image %s is built" % diskimage.name)
|
||||
@ -760,7 +763,8 @@ class BuildWorker(BaseWorker):
|
||||
if self._statsd:
|
||||
# record stats on the size of each image we create
|
||||
for ext in img_types.split(','):
|
||||
key = 'nodepool.dib_image_build.%s.%s.size' % (diskimage.name, ext)
|
||||
key = 'nodepool.dib_image_build.%s.%s.size' % (
|
||||
diskimage.name, ext)
|
||||
# A bit tricky because these image files may be sparse
|
||||
# files; we only want the true size of the file for
|
||||
# purposes of watching if we've added too much stuff
|
||||
@ -780,9 +784,13 @@ class BuildWorker(BaseWorker):
|
||||
self._running = True
|
||||
while self._running:
|
||||
# Don't do work if we've lost communication with the ZK cluster
|
||||
did_suspend = False
|
||||
while self._zk and (self._zk.suspended or self._zk.lost):
|
||||
did_suspend = True
|
||||
self.log.info("ZooKeeper suspended. Waiting")
|
||||
time.sleep(SUSPEND_WAIT_TIME)
|
||||
if did_suspend:
|
||||
self.log.info("ZooKeeper available. Resuming")
|
||||
|
||||
try:
|
||||
self._run()
|
||||
@ -798,6 +806,8 @@ class BuildWorker(BaseWorker):
|
||||
'''
|
||||
# NOTE: For the first iteration, we expect self._config to be None
|
||||
new_config = nodepool_config.loadConfig(self._config_path)
|
||||
if self._secure_path:
|
||||
nodepool_config.loadSecureConfig(new_config, self._secure_path)
|
||||
if not self._config:
|
||||
self._config = new_config
|
||||
|
||||
@ -809,8 +819,10 @@ class BuildWorker(BaseWorker):
|
||||
|
||||
|
||||
class UploadWorker(BaseWorker):
|
||||
def __init__(self, name, config_path, interval, zk):
|
||||
super(UploadWorker, self).__init__(config_path, interval, zk)
|
||||
def __init__(self, name, builder_id, config_path, secure_path,
|
||||
interval, zk):
|
||||
super(UploadWorker, self).__init__(builder_id, config_path,
|
||||
secure_path, interval, zk)
|
||||
self.log = logging.getLogger("nodepool.builder.UploadWorker.%s" % name)
|
||||
self.name = 'UploadWorker.%s' % name
|
||||
|
||||
@ -819,6 +831,8 @@ class UploadWorker(BaseWorker):
|
||||
Reload the nodepool configuration file.
|
||||
'''
|
||||
new_config = nodepool_config.loadConfig(self._config_path)
|
||||
if self._secure_path:
|
||||
nodepool_config.loadSecureConfig(new_config, self._secure_path)
|
||||
if not self._config:
|
||||
self._config = new_config
|
||||
|
||||
@ -827,7 +841,8 @@ class UploadWorker(BaseWorker):
|
||||
use_taskmanager=False)
|
||||
self._config = new_config
|
||||
|
||||
def _uploadImage(self, build_id, upload_id, image_name, images, provider):
|
||||
def _uploadImage(self, build_id, upload_id, image_name, images, provider,
|
||||
username):
|
||||
'''
|
||||
Upload a local DIB image build to a provider.
|
||||
|
||||
@ -837,6 +852,7 @@ class UploadWorker(BaseWorker):
|
||||
:param list images: A list of DibImageFile objects from this build
|
||||
that available for uploading.
|
||||
:param provider: The provider from the parsed config file.
|
||||
:param username:
|
||||
'''
|
||||
start_time = time.time()
|
||||
timestamp = int(start_time)
|
||||
@ -858,19 +874,15 @@ class UploadWorker(BaseWorker):
|
||||
|
||||
filename = image.to_path(self._config.imagesdir, with_extension=True)
|
||||
|
||||
dummy_image = type('obj', (object,),
|
||||
{'name': image_name, 'id': image.image_id})
|
||||
|
||||
ext_image_name = provider.template_hostname.format(
|
||||
provider=provider, image=dummy_image,
|
||||
timestamp=str(timestamp)
|
||||
ext_image_name = provider.image_name_format.format(
|
||||
image_name=image_name, timestamp=str(timestamp)
|
||||
)
|
||||
|
||||
self.log.info("Uploading DIB image build %s from %s to %s" %
|
||||
(build_id, filename, provider.name))
|
||||
|
||||
manager = self._config.provider_managers[provider.name]
|
||||
provider_image = provider.images.get(image_name)
|
||||
provider_image = provider.diskimages.get(image_name)
|
||||
if provider_image is None:
|
||||
raise exceptions.BuilderInvalidCommandError(
|
||||
"Could not find matching provider image for %s" % image_name
|
||||
@ -910,6 +922,9 @@ class UploadWorker(BaseWorker):
|
||||
data.state = zk.READY
|
||||
data.external_id = external_id
|
||||
data.external_name = ext_image_name
|
||||
data.format = image.extension
|
||||
data.username = username
|
||||
|
||||
return data
|
||||
|
||||
def _checkForProviderUploads(self):
|
||||
@ -920,12 +935,12 @@ class UploadWorker(BaseWorker):
|
||||
to providers, do the upload if they are available on the local disk.
|
||||
'''
|
||||
for provider in self._config.providers.values():
|
||||
for image in provider.images.values():
|
||||
for image in provider.diskimages.values():
|
||||
uploaded = False
|
||||
|
||||
# Check if we've been told to shutdown
|
||||
# or if ZK connection is suspended
|
||||
if not self.running or self._zk.suspended or self._zk.lost:
|
||||
if not self._running or self._zk.suspended or self._zk.lost:
|
||||
return
|
||||
try:
|
||||
uploaded = self._checkProviderImageUpload(provider, image)
|
||||
@ -952,7 +967,7 @@ class UploadWorker(BaseWorker):
|
||||
:returns: True if an upload was attempted, False otherwise.
|
||||
'''
|
||||
# Check if image uploads are paused.
|
||||
if provider.images.get(image.name).pause:
|
||||
if provider.diskimages.get(image.name).pause:
|
||||
return False
|
||||
|
||||
# Search for the most recent 'ready' image build
|
||||
@ -1003,11 +1018,14 @@ class UploadWorker(BaseWorker):
|
||||
# New upload number with initial state 'uploading'
|
||||
data = zk.ImageUpload()
|
||||
data.state = zk.UPLOADING
|
||||
data.username = build.username
|
||||
|
||||
upnum = self._zk.storeImageUpload(
|
||||
image.name, build.id, provider.name, data)
|
||||
|
||||
data = self._uploadImage(build.id, upnum, image.name,
|
||||
local_images, provider)
|
||||
local_images, provider,
|
||||
build.username)
|
||||
|
||||
# Set final state
|
||||
self._zk.storeImageUpload(image.name, build.id,
|
||||
@ -1025,9 +1043,13 @@ class UploadWorker(BaseWorker):
|
||||
self._running = True
|
||||
while self._running:
|
||||
# Don't do work if we've lost communication with the ZK cluster
|
||||
did_suspend = False
|
||||
while self._zk and (self._zk.suspended or self._zk.lost):
|
||||
did_suspend = True
|
||||
self.log.info("ZooKeeper suspended. Waiting")
|
||||
time.sleep(SUSPEND_WAIT_TIME)
|
||||
if did_suspend:
|
||||
self.log.info("ZooKeeper available. Resuming")
|
||||
|
||||
try:
|
||||
self._reloadConfig()
|
||||
@ -1051,15 +1073,19 @@ class NodePoolBuilder(object):
|
||||
'''
|
||||
log = logging.getLogger("nodepool.builder.NodePoolBuilder")
|
||||
|
||||
def __init__(self, config_path, num_builders=1, num_uploaders=4):
|
||||
def __init__(self, config_path, secure_path=None,
|
||||
num_builders=1, num_uploaders=4, fake=False):
|
||||
'''
|
||||
Initialize the NodePoolBuilder object.
|
||||
|
||||
:param str config_path: Path to configuration file.
|
||||
:param str secure_path: Path to secure configuration file.
|
||||
:param int num_builders: Number of build workers to start.
|
||||
:param int num_uploaders: Number of upload workers to start.
|
||||
:param bool fake: Whether to fake the image builds.
|
||||
'''
|
||||
self._config_path = config_path
|
||||
self._secure_path = secure_path
|
||||
self._config = None
|
||||
self._num_builders = num_builders
|
||||
self._build_workers = []
|
||||
@ -1070,7 +1096,11 @@ class NodePoolBuilder(object):
|
||||
self.cleanup_interval = 60
|
||||
self.build_interval = 10
|
||||
self.upload_interval = 10
|
||||
self.dib_cmd = 'disk-image-create'
|
||||
if fake:
|
||||
self.dib_cmd = os.path.join(os.path.dirname(__file__), '..',
|
||||
'nodepool/tests/fake-image-create')
|
||||
else:
|
||||
self.dib_cmd = 'disk-image-create'
|
||||
self.zk = None
|
||||
|
||||
# This lock is needed because the run() method is started in a
|
||||
@ -1079,21 +1109,34 @@ class NodePoolBuilder(object):
|
||||
# startup process has completed.
|
||||
self._start_lock = threading.Lock()
|
||||
|
||||
#=======================================================================
|
||||
# ======================================================================
|
||||
# Private methods
|
||||
#=======================================================================
|
||||
# ======================================================================
|
||||
|
||||
def _getBuilderID(self, id_file):
|
||||
if not os.path.exists(id_file):
|
||||
with open(id_file, "w") as f:
|
||||
builder_id = str(uuid.uuid4())
|
||||
f.write(builder_id)
|
||||
return builder_id
|
||||
|
||||
with open(id_file, "r") as f:
|
||||
builder_id = f.read()
|
||||
return builder_id
|
||||
|
||||
def _getAndValidateConfig(self):
|
||||
config = nodepool_config.loadConfig(self._config_path)
|
||||
if self._secure_path:
|
||||
nodepool_config.loadSecureConfig(config, self._secure_path)
|
||||
if not config.zookeeper_servers.values():
|
||||
raise RuntimeError('No ZooKeeper servers specified in config.')
|
||||
if not config.imagesdir:
|
||||
raise RuntimeError('No images-dir specified in config.')
|
||||
return config
|
||||
|
||||
#=======================================================================
|
||||
# ======================================================================
|
||||
# Public methods
|
||||
#=======================================================================
|
||||
# ======================================================================
|
||||
|
||||
def start(self):
|
||||
'''
|
||||
@ -1110,28 +1153,36 @@ class NodePoolBuilder(object):
|
||||
self._config = self._getAndValidateConfig()
|
||||
self._running = True
|
||||
|
||||
builder_id_file = os.path.join(self._config.imagesdir,
|
||||
"builder_id.txt")
|
||||
builder_id = self._getBuilderID(builder_id_file)
|
||||
|
||||
# All worker threads share a single ZooKeeper instance/connection.
|
||||
self.zk = zk.ZooKeeper()
|
||||
self.zk.connect(self._config.zookeeper_servers.values())
|
||||
self.zk.connect(list(self._config.zookeeper_servers.values()))
|
||||
|
||||
self.log.debug('Starting listener for build jobs')
|
||||
|
||||
# Create build and upload worker objects
|
||||
for i in range(self._num_builders):
|
||||
w = BuildWorker(i, self._config_path, self.build_interval,
|
||||
self.zk, self.dib_cmd)
|
||||
w = BuildWorker(i, builder_id,
|
||||
self._config_path, self._secure_path,
|
||||
self.build_interval, self.zk, self.dib_cmd)
|
||||
w.start()
|
||||
self._build_workers.append(w)
|
||||
|
||||
for i in range(self._num_uploaders):
|
||||
w = UploadWorker(i, self._config_path, self.upload_interval,
|
||||
self.zk)
|
||||
w = UploadWorker(i, builder_id,
|
||||
self._config_path, self._secure_path,
|
||||
self.upload_interval, self.zk)
|
||||
w.start()
|
||||
self._upload_workers.append(w)
|
||||
|
||||
if self.cleanup_interval > 0:
|
||||
self._janitor = CleanupWorker(
|
||||
0, self._config_path, self.cleanup_interval, self.zk)
|
||||
0, builder_id,
|
||||
self._config_path, self._secure_path,
|
||||
self.cleanup_interval, self.zk)
|
||||
self._janitor.start()
|
||||
|
||||
# Wait until all threads are running. Otherwise, we have a race
|
||||
@ -1154,7 +1205,14 @@ class NodePoolBuilder(object):
|
||||
'''
|
||||
with self._start_lock:
|
||||
self.log.debug("Stopping. NodePoolBuilder shutting down workers")
|
||||
workers = self._build_workers + self._upload_workers
|
||||
# Note we do not add the upload workers to this list intentionally.
|
||||
# The reason for this is that uploads can take many hours and there
|
||||
# is no good way to stop the blocking writes performed by the
|
||||
# uploads in order to join() below on a reasonable amount of time.
|
||||
# Killing the process will stop the upload then both the record
|
||||
# in zk and in the cloud will be deleted by any other running
|
||||
# builders or when this builder starts again.
|
||||
workers = self._build_workers
|
||||
if self._janitor:
|
||||
workers += [self._janitor]
|
||||
for worker in (workers):
|
||||
|
@ -14,6 +14,10 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import argparse
|
||||
import daemon
|
||||
import errno
|
||||
import extras
|
||||
import logging
|
||||
import logging.config
|
||||
import os
|
||||
@ -22,6 +26,37 @@ import sys
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
import yaml
|
||||
|
||||
from nodepool.version import version_info as npd_version_info
|
||||
|
||||
|
||||
# as of python-daemon 1.6 it doesn't bundle pidlockfile anymore
|
||||
# instead it depends on lockfile-0.9.1 which uses pidfile.
|
||||
pid_file_module = extras.try_imports(['daemon.pidlockfile', 'daemon.pidfile'])
|
||||
|
||||
|
||||
def is_pidfile_stale(pidfile):
|
||||
""" Determine whether a PID file is stale.
|
||||
|
||||
Return 'True' ("stale") if the contents of the PID file are
|
||||
valid but do not match the PID of a currently-running process;
|
||||
otherwise return 'False'.
|
||||
|
||||
"""
|
||||
result = False
|
||||
|
||||
pidfile_pid = pidfile.read_pid()
|
||||
if pidfile_pid is not None:
|
||||
try:
|
||||
os.kill(pidfile_pid, 0)
|
||||
except OSError as exc:
|
||||
if exc.errno == errno.ESRCH:
|
||||
# The specified PID does not exist
|
||||
result = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def stack_dump_handler(signum, frame):
|
||||
signal.signal(signal.SIGUSR2, signal.SIG_IGN)
|
||||
@ -45,17 +80,99 @@ def stack_dump_handler(signum, frame):
|
||||
|
||||
class NodepoolApp(object):
|
||||
|
||||
app_name = None
|
||||
app_description = 'Node pool.'
|
||||
|
||||
def __init__(self):
|
||||
self.parser = None
|
||||
self.args = None
|
||||
|
||||
def create_parser(self):
|
||||
parser = argparse.ArgumentParser(description=self.app_description)
|
||||
|
||||
parser.add_argument('-l',
|
||||
dest='logconfig',
|
||||
help='path to log config file')
|
||||
|
||||
parser.add_argument('--version',
|
||||
action='version',
|
||||
version=npd_version_info.version_string())
|
||||
|
||||
return parser
|
||||
|
||||
def setup_logging(self):
|
||||
if self.args.logconfig:
|
||||
fp = os.path.expanduser(self.args.logconfig)
|
||||
|
||||
if not os.path.exists(fp):
|
||||
raise Exception("Unable to read logging config file at %s" %
|
||||
fp)
|
||||
logging.config.fileConfig(fp)
|
||||
m = "Unable to read logging config file at %s" % fp
|
||||
raise Exception(m)
|
||||
|
||||
if os.path.splitext(fp)[1] in ('.yml', '.yaml'):
|
||||
with open(fp, 'r') as f:
|
||||
logging.config.dictConfig(yaml.safe_load(f))
|
||||
|
||||
else:
|
||||
logging.config.fileConfig(fp)
|
||||
|
||||
else:
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s %(levelname)s %(name)s: '
|
||||
'%(message)s')
|
||||
m = '%(asctime)s %(levelname)s %(name)s: %(message)s'
|
||||
logging.basicConfig(level=logging.DEBUG, format=m)
|
||||
|
||||
def _main(self, argv=None):
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
self.parser = self.create_parser()
|
||||
self.args = self.parser.parse_args()
|
||||
return self._do_run()
|
||||
|
||||
def _do_run(self):
|
||||
# NOTE(jamielennox): setup logging a bit late so it's not done until
|
||||
# after a DaemonContext is created.
|
||||
self.setup_logging()
|
||||
return self.run()
|
||||
|
||||
@classmethod
|
||||
def main(cls, argv=None):
|
||||
return cls()._main(argv=argv)
|
||||
|
||||
def run(self):
|
||||
"""The app's primary function, override it with your logic."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class NodepoolDaemonApp(NodepoolApp):
|
||||
|
||||
def create_parser(self):
|
||||
parser = super(NodepoolDaemonApp, self).create_parser()
|
||||
|
||||
parser.add_argument('-p',
|
||||
dest='pidfile',
|
||||
help='path to pid file',
|
||||
default='/var/run/nodepool/%s.pid' % self.app_name)
|
||||
|
||||
parser.add_argument('-d',
|
||||
dest='nodaemon',
|
||||
action='store_true',
|
||||
help='do not run as a daemon')
|
||||
|
||||
return parser
|
||||
|
||||
def _do_run(self):
|
||||
if self.args.nodaemon:
|
||||
return super(NodepoolDaemonApp, self)._do_run()
|
||||
|
||||
else:
|
||||
pid = pid_file_module.TimeoutPIDLockFile(self.args.pidfile, 10)
|
||||
|
||||
if is_pidfile_stale(pid):
|
||||
pid.break_lock()
|
||||
|
||||
with daemon.DaemonContext(pidfile=pid):
|
||||
return super(NodepoolDaemonApp, self)._do_run()
|
||||
|
||||
@classmethod
|
||||
def main(cls, argv=None):
|
||||
signal.signal(signal.SIGUSR2, stack_dump_handler)
|
||||
return super(NodepoolDaemonApp, cls).main(argv)
|
||||
|
@ -12,56 +12,51 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import argparse
|
||||
import extras
|
||||
import signal
|
||||
import sys
|
||||
|
||||
import daemon
|
||||
|
||||
from nodepool import builder
|
||||
import nodepool.cmd
|
||||
|
||||
|
||||
# as of python-daemon 1.6 it doesn't bundle pidlockfile anymore
|
||||
# instead it depends on lockfile-0.9.1 which uses pidfile.
|
||||
pid_file_module = extras.try_imports(['daemon.pidlockfile', 'daemon.pidfile'])
|
||||
class NodePoolBuilderApp(nodepool.cmd.NodepoolDaemonApp):
|
||||
|
||||
class NodePoolBuilderApp(nodepool.cmd.NodepoolApp):
|
||||
app_name = 'nodepool-builder'
|
||||
app_description = 'NodePool Image Builder.'
|
||||
|
||||
def sigint_handler(self, signal, frame):
|
||||
self.nb.stop()
|
||||
sys.exit(0)
|
||||
|
||||
def parse_arguments(self):
|
||||
parser = argparse.ArgumentParser(description='NodePool Image Builder.')
|
||||
def create_parser(self):
|
||||
parser = super(NodePoolBuilderApp, self).create_parser()
|
||||
|
||||
parser.add_argument('-c', dest='config',
|
||||
default='/etc/nodepool/nodepool.yaml',
|
||||
help='path to config file')
|
||||
parser.add_argument('-l', dest='logconfig',
|
||||
help='path to log config file')
|
||||
parser.add_argument('-p', dest='pidfile',
|
||||
help='path to pid file',
|
||||
default='/var/run/nodepool-builder/'
|
||||
'nodepool-builder.pid')
|
||||
parser.add_argument('-d', dest='nodaemon', action='store_true',
|
||||
help='do not run as a daemon')
|
||||
parser.add_argument('-s', dest='secure',
|
||||
help='path to secure config file')
|
||||
parser.add_argument('--build-workers', dest='build_workers',
|
||||
default=1, help='number of build workers',
|
||||
type=int)
|
||||
parser.add_argument('--upload-workers', dest='upload_workers',
|
||||
default=4, help='number of upload workers',
|
||||
type=int)
|
||||
self.args = parser.parse_args()
|
||||
parser.add_argument('--fake', action='store_true',
|
||||
help='Do not actually run diskimage-builder '
|
||||
'(used for testing)')
|
||||
return parser
|
||||
|
||||
def main(self):
|
||||
self.setup_logging()
|
||||
def run(self):
|
||||
self.nb = builder.NodePoolBuilder(
|
||||
self.args.config, self.args.build_workers,
|
||||
self.args.upload_workers)
|
||||
self.args.config,
|
||||
secure_path=self.args.secure,
|
||||
num_builders=self.args.build_workers,
|
||||
num_uploaders=self.args.upload_workers,
|
||||
fake=self.args.fake)
|
||||
|
||||
signal.signal(signal.SIGINT, self.sigint_handler)
|
||||
signal.signal(signal.SIGUSR2, nodepool.cmd.stack_dump_handler)
|
||||
|
||||
self.nb.start()
|
||||
|
||||
while True:
|
||||
@ -69,15 +64,7 @@ class NodePoolBuilderApp(nodepool.cmd.NodepoolApp):
|
||||
|
||||
|
||||
def main():
|
||||
app = NodePoolBuilderApp()
|
||||
app.parse_arguments()
|
||||
|
||||
if app.args.nodaemon:
|
||||
app.main()
|
||||
else:
|
||||
pid = pid_file_module.TimeoutPIDLockFile(app.args.pidfile, 10)
|
||||
with daemon.DaemonContext(pidfile=pid):
|
||||
app.main()
|
||||
return NodePoolBuilderApp.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -14,6 +14,8 @@ import logging
|
||||
import voluptuous as v
|
||||
import yaml
|
||||
|
||||
from nodepool.config import get_provider_config
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -24,88 +26,19 @@ class ConfigValidator:
|
||||
self.config_file = config_file
|
||||
|
||||
def validate(self):
|
||||
cron = {
|
||||
'check': str,
|
||||
'cleanup': str,
|
||||
}
|
||||
|
||||
images = {
|
||||
'name': str,
|
||||
'pause': bool,
|
||||
'min-ram': int,
|
||||
'name-filter': str,
|
||||
'key-name': str,
|
||||
'diskimage': str,
|
||||
'meta': dict,
|
||||
'username': str,
|
||||
'user-home': str,
|
||||
'private-key': str,
|
||||
'config-drive': bool,
|
||||
}
|
||||
|
||||
old_network = {
|
||||
'net-id': str,
|
||||
'net-label': str,
|
||||
}
|
||||
|
||||
network = {
|
||||
provider = {
|
||||
'name': v.Required(str),
|
||||
'public': bool, # Ignored, but kept for backwards compat
|
||||
'driver': str,
|
||||
'max-concurrency': int,
|
||||
}
|
||||
|
||||
providers = {
|
||||
label = {
|
||||
'name': str,
|
||||
'region-name': str,
|
||||
'service-type': str,
|
||||
'service-name': str,
|
||||
'availability-zones': [str],
|
||||
'cloud': str,
|
||||
'username': str,
|
||||
'password': str,
|
||||
'auth-url': str,
|
||||
'project-id': str,
|
||||
'project-name': str,
|
||||
'max-servers': int,
|
||||
'pool': str, # Ignored, but kept for backwards compat
|
||||
'image-type': str,
|
||||
'networks': [v.Any(old_network, network)],
|
||||
'ipv6-preferred': bool,
|
||||
'boot-timeout': int,
|
||||
'api-timeout': int,
|
||||
'launch-timeout': int,
|
||||
'nodepool-id': str,
|
||||
'rate': float,
|
||||
'images': [images],
|
||||
'template-hostname': str,
|
||||
'clean-floating-ips': bool,
|
||||
}
|
||||
|
||||
labels = {
|
||||
'name': str,
|
||||
'image': str,
|
||||
'min-ready': int,
|
||||
'ready-script': str,
|
||||
'subnodes': int,
|
||||
'providers': [{
|
||||
'name': str,
|
||||
}],
|
||||
'max-ready-age': int,
|
||||
}
|
||||
|
||||
targets = {
|
||||
'name': str,
|
||||
'hostname': str,
|
||||
'subnode-hostname': str,
|
||||
'assign-via-gearman': bool,
|
||||
'jenkins': {
|
||||
'url': str,
|
||||
'user': str,
|
||||
'apikey': str,
|
||||
'credentials-id': str,
|
||||
'test-job': str
|
||||
}
|
||||
}
|
||||
|
||||
diskimages = {
|
||||
diskimage = {
|
||||
'name': str,
|
||||
'pause': bool,
|
||||
'elements': [str],
|
||||
@ -113,27 +46,26 @@ class ConfigValidator:
|
||||
'release': v.Any(str, int),
|
||||
'rebuild-age': int,
|
||||
'env-vars': {str: str},
|
||||
'username': str,
|
||||
}
|
||||
|
||||
webapp = {
|
||||
'port': int,
|
||||
'listen_address': str,
|
||||
}
|
||||
|
||||
top_level = {
|
||||
'webapp': webapp,
|
||||
'elements-dir': str,
|
||||
'images-dir': str,
|
||||
'dburi': str,
|
||||
'zmq-publishers': [str],
|
||||
'gearman-servers': [{
|
||||
'host': str,
|
||||
'port': int,
|
||||
}],
|
||||
'zookeeper-servers': [{
|
||||
'host': str,
|
||||
'port': int,
|
||||
'chroot': str,
|
||||
}],
|
||||
'cron': cron,
|
||||
'providers': [providers],
|
||||
'labels': [labels],
|
||||
'targets': [targets],
|
||||
'diskimages': [diskimages],
|
||||
'providers': list,
|
||||
'labels': [label],
|
||||
'diskimages': [diskimage],
|
||||
}
|
||||
|
||||
log.info("validating %s" % self.config_file)
|
||||
@ -142,12 +74,6 @@ class ConfigValidator:
|
||||
# validate the overall schema
|
||||
schema = v.Schema(top_level)
|
||||
schema(config)
|
||||
|
||||
# labels must list valid providers
|
||||
all_providers = [p['name'] for p in config['providers']]
|
||||
for label in config['labels']:
|
||||
for provider in label['providers']:
|
||||
if not provider['name'] in all_providers:
|
||||
raise AssertionError('label %s requests '
|
||||
'non-existent provider %s'
|
||||
% (label['name'], provider['name']))
|
||||
for provider_dict in config.get('providers', []):
|
||||
provider_schema = get_provider_config(provider_dict).get_schema()
|
||||
provider_schema.extend(provider)(provider_dict)
|
||||
|
81
nodepool/cmd/launcher.py
Executable file
81
nodepool/cmd/launcher.py
Executable file
@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright 2012 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
#
|
||||
# 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
|
||||
import signal
|
||||
|
||||
import nodepool.cmd
|
||||
import nodepool.launcher
|
||||
import nodepool.webapp
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NodePoolLauncherApp(nodepool.cmd.NodepoolDaemonApp):
|
||||
|
||||
app_name = 'nodepool'
|
||||
|
||||
def create_parser(self):
|
||||
parser = super(NodePoolLauncherApp, self).create_parser()
|
||||
|
||||
parser.add_argument('-c', dest='config',
|
||||
default='/etc/nodepool/nodepool.yaml',
|
||||
help='path to config file')
|
||||
parser.add_argument('-s', dest='secure',
|
||||
help='path to secure file')
|
||||
parser.add_argument('--no-webapp', action='store_true')
|
||||
return parser
|
||||
|
||||
def exit_handler(self, signum, frame):
|
||||
self.pool.stop()
|
||||
if not self.args.no_webapp:
|
||||
self.webapp.stop()
|
||||
sys.exit(0)
|
||||
|
||||
def term_handler(self, signum, frame):
|
||||
os._exit(0)
|
||||
|
||||
def run(self):
|
||||
self.pool = nodepool.launcher.NodePool(self.args.secure,
|
||||
self.args.config)
|
||||
if not self.args.no_webapp:
|
||||
config = self.pool.loadConfig()
|
||||
self.webapp = nodepool.webapp.WebApp(self.pool,
|
||||
**config.webapp)
|
||||
|
||||
signal.signal(signal.SIGINT, self.exit_handler)
|
||||
# For back compatibility:
|
||||
signal.signal(signal.SIGUSR1, self.exit_handler)
|
||||
|
||||
signal.signal(signal.SIGTERM, self.term_handler)
|
||||
|
||||
self.pool.start()
|
||||
|
||||
if not self.args.no_webapp:
|
||||
self.webapp.start()
|
||||
|
||||
while True:
|
||||
signal.pause()
|
||||
|
||||
|
||||
def main():
|
||||
return NodePoolLauncherApp.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
218
nodepool/cmd/nodepoolcmd.py
Normal file → Executable file
218
nodepool/cmd/nodepoolcmd.py
Normal file → Executable file
@ -14,37 +14,31 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import argparse
|
||||
import logging.config
|
||||
import sys
|
||||
|
||||
from nodepool import nodedb
|
||||
from nodepool import nodepool
|
||||
from prettytable import PrettyTable
|
||||
|
||||
from nodepool import launcher
|
||||
from nodepool import provider_manager
|
||||
from nodepool import status
|
||||
from nodepool import zk
|
||||
from nodepool.cmd import NodepoolApp
|
||||
from nodepool.version import version_info as npc_version_info
|
||||
from config_validator import ConfigValidator
|
||||
from prettytable import PrettyTable
|
||||
from nodepool.cmd.config_validator import ConfigValidator
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NodePoolCmd(NodepoolApp):
|
||||
|
||||
def parse_arguments(self):
|
||||
parser = argparse.ArgumentParser(description='Node pool.')
|
||||
def create_parser(self):
|
||||
parser = super(NodePoolCmd, self).create_parser()
|
||||
|
||||
parser.add_argument('-c', dest='config',
|
||||
default='/etc/nodepool/nodepool.yaml',
|
||||
help='path to config file')
|
||||
parser.add_argument('-s', dest='secure',
|
||||
default='/etc/nodepool/secure.conf',
|
||||
help='path to secure file')
|
||||
parser.add_argument('-l', dest='logconfig',
|
||||
help='path to log config file')
|
||||
parser.add_argument('--version', action='version',
|
||||
version=npc_version_info.version_string(),
|
||||
help='show version')
|
||||
parser.add_argument('--debug', dest='debug', action='store_true',
|
||||
help='show DEBUG level logging')
|
||||
|
||||
@ -55,6 +49,9 @@ class NodePoolCmd(NodepoolApp):
|
||||
|
||||
cmd_list = subparsers.add_parser('list', help='list nodes')
|
||||
cmd_list.set_defaults(func=self.list)
|
||||
cmd_list.add_argument('--detail', action='store_true',
|
||||
help='Output detailed node info')
|
||||
|
||||
cmd_image_list = subparsers.add_parser(
|
||||
'image-list', help='list images from providers')
|
||||
cmd_image_list.set_defaults(func=self.image_list)
|
||||
@ -70,13 +67,6 @@ class NodePoolCmd(NodepoolApp):
|
||||
cmd_image_build.add_argument('image', help='image name')
|
||||
cmd_image_build.set_defaults(func=self.image_build)
|
||||
|
||||
cmd_alien_list = subparsers.add_parser(
|
||||
'alien-list',
|
||||
help='list nodes not accounted for by nodepool')
|
||||
cmd_alien_list.set_defaults(func=self.alien_list)
|
||||
cmd_alien_list.add_argument('provider', help='provider name',
|
||||
nargs='?')
|
||||
|
||||
cmd_alien_image_list = subparsers.add_parser(
|
||||
'alien-image-list',
|
||||
help='list images not accounted for by nodepool')
|
||||
@ -90,7 +80,8 @@ class NodePoolCmd(NodepoolApp):
|
||||
cmd_hold.set_defaults(func=self.hold)
|
||||
cmd_hold.add_argument('id', help='node id')
|
||||
cmd_hold.add_argument('--reason',
|
||||
help='Optional reason this node is held')
|
||||
help='Reason this node is held',
|
||||
required=True)
|
||||
|
||||
cmd_delete = subparsers.add_parser(
|
||||
'delete',
|
||||
@ -116,7 +107,8 @@ class NodePoolCmd(NodepoolApp):
|
||||
|
||||
cmd_dib_image_delete = subparsers.add_parser(
|
||||
'dib-image-delete',
|
||||
help='delete image built with diskimage-builder')
|
||||
help='Delete a dib built image from disk along with all cloud '
|
||||
'uploads of this image')
|
||||
cmd_dib_image_delete.set_defaults(func=self.dib_image_delete)
|
||||
cmd_dib_image_delete.add_argument('id', help='dib image id')
|
||||
|
||||
@ -125,47 +117,39 @@ class NodePoolCmd(NodepoolApp):
|
||||
help='Validate configuration file')
|
||||
cmd_config_validate.set_defaults(func=self.config_validate)
|
||||
|
||||
cmd_job_list = subparsers.add_parser('job-list', help='list jobs')
|
||||
cmd_job_list.set_defaults(func=self.job_list)
|
||||
cmd_request_list = subparsers.add_parser(
|
||||
'request-list',
|
||||
help='list the current node requests')
|
||||
cmd_request_list.set_defaults(func=self.request_list)
|
||||
|
||||
cmd_job_create = subparsers.add_parser('job-create', help='create job')
|
||||
cmd_job_create.add_argument(
|
||||
'name',
|
||||
help='job name')
|
||||
cmd_job_create.add_argument('--hold-on-failure',
|
||||
help='number of nodes to hold when this job fails')
|
||||
cmd_job_create.set_defaults(func=self.job_create)
|
||||
|
||||
cmd_job_delete = subparsers.add_parser(
|
||||
'job-delete',
|
||||
help='delete job')
|
||||
cmd_job_delete.set_defaults(func=self.job_delete)
|
||||
cmd_job_delete.add_argument('id', help='job id')
|
||||
|
||||
self.args = parser.parse_args()
|
||||
return parser
|
||||
|
||||
def setup_logging(self):
|
||||
# NOTE(jamielennox): This should just be the same as other apps
|
||||
if self.args.debug:
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s %(levelname)s %(name)s: '
|
||||
'%(message)s')
|
||||
m = '%(asctime)s %(levelname)s %(name)s: %(message)s'
|
||||
logging.basicConfig(level=logging.DEBUG, format=m)
|
||||
|
||||
elif self.args.logconfig:
|
||||
NodepoolApp.setup_logging(self)
|
||||
super(NodePoolCmd, self).setup_logging()
|
||||
|
||||
else:
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(asctime)s %(levelname)s %(name)s: '
|
||||
'%(message)s')
|
||||
m = '%(asctime)s %(levelname)s %(name)s: %(message)s'
|
||||
logging.basicConfig(level=logging.INFO, format=m)
|
||||
|
||||
l = logging.getLogger('kazoo')
|
||||
l.setLevel(logging.WARNING)
|
||||
|
||||
def list(self, node_id=None):
|
||||
print status.node_list(self.pool.getDB(), node_id)
|
||||
def list(self, node_id=None, detail=False):
|
||||
if hasattr(self.args, 'detail'):
|
||||
detail = self.args.detail
|
||||
print(status.node_list(self.zk, node_id, detail))
|
||||
|
||||
def dib_image_list(self):
|
||||
print status.dib_image_list(self.zk)
|
||||
print(status.dib_image_list(self.zk))
|
||||
|
||||
def image_list(self):
|
||||
print status.image_list(self.zk)
|
||||
print(status.image_list(self.zk))
|
||||
|
||||
def image_build(self, diskimage=None):
|
||||
diskimage = diskimage or self.args.image
|
||||
@ -180,31 +164,8 @@ class NodePoolCmd(NodepoolApp):
|
||||
|
||||
self.zk.submitBuildRequest(diskimage)
|
||||
|
||||
def alien_list(self):
|
||||
self.pool.reconfigureManagers(self.pool.config, False)
|
||||
|
||||
t = PrettyTable(["Provider", "Hostname", "Server ID", "IP"])
|
||||
t.align = 'l'
|
||||
with self.pool.getDB().getSession() as session:
|
||||
for provider in self.pool.config.providers.values():
|
||||
if (self.args.provider and
|
||||
provider.name != self.args.provider):
|
||||
continue
|
||||
manager = self.pool.getProviderManager(provider)
|
||||
|
||||
try:
|
||||
for server in manager.listServers():
|
||||
if not session.getNodeByExternalID(
|
||||
provider.name, server['id']):
|
||||
t.add_row([provider.name, server['name'],
|
||||
server['id'], server['public_v4']])
|
||||
except Exception as e:
|
||||
log.warning("Exception listing aliens for %s: %s"
|
||||
% (provider.name, str(e.message)))
|
||||
print t
|
||||
|
||||
def alien_image_list(self):
|
||||
self.pool.reconfigureManagers(self.pool.config, False)
|
||||
self.pool.updateConfig()
|
||||
|
||||
t = PrettyTable(["Provider", "Name", "Image ID"])
|
||||
t.align = 'l'
|
||||
@ -213,7 +174,7 @@ class NodePoolCmd(NodepoolApp):
|
||||
if (self.args.provider and
|
||||
provider.name != self.args.provider):
|
||||
continue
|
||||
manager = self.pool.getProviderManager(provider)
|
||||
manager = self.pool.getProviderManager(provider.name)
|
||||
|
||||
# Build list of provider images as known by the provider
|
||||
provider_images = []
|
||||
@ -227,11 +188,11 @@ class NodePoolCmd(NodepoolApp):
|
||||
if 'nodepool_build_id' in image['properties']]
|
||||
except Exception as e:
|
||||
log.warning("Exception listing alien images for %s: %s"
|
||||
% (provider.name, str(e.message)))
|
||||
% (provider.name, str(e)))
|
||||
|
||||
alien_ids = []
|
||||
uploads = []
|
||||
for image in provider.images:
|
||||
for image in provider.diskimages:
|
||||
# Build list of provider images as recorded in ZK
|
||||
for bnum in self.zk.getBuildNumbers(image):
|
||||
uploads.extend(
|
||||
@ -249,30 +210,46 @@ class NodePoolCmd(NodepoolApp):
|
||||
if image['id'] in alien_ids:
|
||||
t.add_row([provider.name, image['name'], image['id']])
|
||||
|
||||
print t
|
||||
print(t)
|
||||
|
||||
def hold(self):
|
||||
node_id = None
|
||||
with self.pool.getDB().getSession() as session:
|
||||
node = session.getNode(self.args.id)
|
||||
node.state = nodedb.HOLD
|
||||
if self.args.reason:
|
||||
node.comment = self.args.reason
|
||||
node_id = node.id
|
||||
self.list(node_id=node_id)
|
||||
node = self.zk.getNode(self.args.id)
|
||||
if not node:
|
||||
print("Node id %s not found" % self.args.id)
|
||||
return
|
||||
|
||||
node.state = zk.HOLD
|
||||
node.comment = self.args.reason
|
||||
print("Waiting for lock...")
|
||||
self.zk.lockNode(node, blocking=True)
|
||||
self.zk.storeNode(node)
|
||||
self.zk.unlockNode(node)
|
||||
self.list(node_id=self.args.id)
|
||||
|
||||
def delete(self):
|
||||
node = self.zk.getNode(self.args.id)
|
||||
if not node:
|
||||
print("Node id %s not found" % self.args.id)
|
||||
return
|
||||
|
||||
self.zk.lockNode(node, blocking=True, timeout=5)
|
||||
|
||||
if self.args.now:
|
||||
self.pool.reconfigureManagers(self.pool.config)
|
||||
with self.pool.getDB().getSession() as session:
|
||||
node = session.getNode(self.args.id)
|
||||
if not node:
|
||||
print "Node %s not found." % self.args.id
|
||||
elif self.args.now:
|
||||
self.pool._deleteNode(session, node)
|
||||
else:
|
||||
node.state = nodedb.DELETE
|
||||
self.list(node_id=node.id)
|
||||
if node.provider not in self.pool.config.providers:
|
||||
print("Provider %s for node %s not defined on this launcher" %
|
||||
(node.provider, node.id))
|
||||
return
|
||||
provider = self.pool.config.providers[node.provider]
|
||||
manager = provider_manager.get_provider(provider, True)
|
||||
manager.start()
|
||||
launcher.NodeDeleter.delete(self.zk, manager, node)
|
||||
manager.stop()
|
||||
else:
|
||||
node.state = zk.DELETING
|
||||
self.zk.storeNode(node)
|
||||
self.zk.unlockNode(node)
|
||||
|
||||
self.list(node_id=node.id)
|
||||
|
||||
def dib_image_delete(self):
|
||||
(image, build_num) = self.args.id.rsplit('-', 1)
|
||||
@ -312,53 +289,38 @@ class NodePoolCmd(NodepoolApp):
|
||||
validator = ConfigValidator(self.args.config)
|
||||
validator.validate()
|
||||
log.info("Configuration validation complete")
|
||||
#TODO(asselin,yolanda): add validation of secure.conf
|
||||
# TODO(asselin,yolanda): add validation of secure.conf
|
||||
|
||||
def job_list(self):
|
||||
t = PrettyTable(["ID", "Name", "Hold on Failure"])
|
||||
t.align = 'l'
|
||||
with self.pool.getDB().getSession() as session:
|
||||
for job in session.getJobs():
|
||||
t.add_row([job.id, job.name, job.hold_on_failure])
|
||||
print t
|
||||
|
||||
def job_create(self):
|
||||
with self.pool.getDB().getSession() as session:
|
||||
session.createJob(self.args.name,
|
||||
hold_on_failure=self.args.hold_on_failure)
|
||||
self.job_list()
|
||||
|
||||
def job_delete(self):
|
||||
with self.pool.getDB().getSession() as session:
|
||||
job = session.getJob(self.args.id)
|
||||
if not job:
|
||||
print "Job %s not found." % self.args.id
|
||||
else:
|
||||
job.delete()
|
||||
def request_list(self):
|
||||
print(status.request_list(self.zk))
|
||||
|
||||
def _wait_for_threads(self, threads):
|
||||
for t in threads:
|
||||
if t:
|
||||
t.join()
|
||||
|
||||
def main(self):
|
||||
def run(self):
|
||||
self.zk = None
|
||||
|
||||
# no arguments, print help messaging, then exit with error(1)
|
||||
if not self.args.command:
|
||||
self.parser.print_help()
|
||||
return 1
|
||||
# commands which do not need to start-up or parse config
|
||||
if self.args.command in ('config-validate'):
|
||||
return self.args.func()
|
||||
|
||||
self.pool = nodepool.NodePool(self.args.secure, self.args.config)
|
||||
self.pool = launcher.NodePool(self.args.secure, self.args.config)
|
||||
config = self.pool.loadConfig()
|
||||
|
||||
# commands needing ZooKeeper
|
||||
if self.args.command in ('image-build', 'dib-image-list',
|
||||
'image-list', 'dib-image-delete',
|
||||
'image-delete', 'alien-image-list'):
|
||||
'image-delete', 'alien-image-list',
|
||||
'list', 'hold', 'delete',
|
||||
'request-list'):
|
||||
self.zk = zk.ZooKeeper()
|
||||
self.zk.connect(config.zookeeper_servers.values())
|
||||
else:
|
||||
self.pool.reconfigureDatabase(config)
|
||||
self.zk.connect(list(config.zookeeper_servers.values()))
|
||||
|
||||
self.pool.setConfig(config)
|
||||
self.args.func()
|
||||
@ -366,11 +328,9 @@ class NodePoolCmd(NodepoolApp):
|
||||
if self.zk:
|
||||
self.zk.disconnect()
|
||||
|
||||
|
||||
def main():
|
||||
npc = NodePoolCmd()
|
||||
npc.parse_arguments()
|
||||
npc.setup_logging()
|
||||
return npc.main()
|
||||
return NodePoolCmd.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -1,160 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright 2012 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import argparse
|
||||
import daemon
|
||||
import errno
|
||||
import extras
|
||||
|
||||
# as of python-daemon 1.6 it doesn't bundle pidlockfile anymore
|
||||
# instead it depends on lockfile-0.9.1 which uses pidfile.
|
||||
pid_file_module = extras.try_imports(['daemon.pidlockfile', 'daemon.pidfile'])
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
|
||||
import nodepool.cmd
|
||||
import nodepool.nodepool
|
||||
import nodepool.webapp
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_pidfile_stale(pidfile):
|
||||
""" Determine whether a PID file is stale.
|
||||
|
||||
Return 'True' ("stale") if the contents of the PID file are
|
||||
valid but do not match the PID of a currently-running process;
|
||||
otherwise return 'False'.
|
||||
|
||||
"""
|
||||
result = False
|
||||
|
||||
pidfile_pid = pidfile.read_pid()
|
||||
if pidfile_pid is not None:
|
||||
try:
|
||||
os.kill(pidfile_pid, 0)
|
||||
except OSError as exc:
|
||||
if exc.errno == errno.ESRCH:
|
||||
# The specified PID does not exist
|
||||
result = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class NodePoolDaemon(nodepool.cmd.NodepoolApp):
|
||||
|
||||
def parse_arguments(self):
|
||||
parser = argparse.ArgumentParser(description='Node pool.')
|
||||
parser.add_argument('-c', dest='config',
|
||||
default='/etc/nodepool/nodepool.yaml',
|
||||
help='path to config file')
|
||||
parser.add_argument('-s', dest='secure',
|
||||
default='/etc/nodepool/secure.conf',
|
||||
help='path to secure file')
|
||||
parser.add_argument('-d', dest='nodaemon', action='store_true',
|
||||
help='do not run as a daemon')
|
||||
parser.add_argument('-l', dest='logconfig',
|
||||
help='path to log config file')
|
||||
parser.add_argument('-p', dest='pidfile',
|
||||
help='path to pid file',
|
||||
default='/var/run/nodepool/nodepool.pid')
|
||||
# TODO(pabelanger): Deprecated flag, remove in the future.
|
||||
parser.add_argument('--no-builder', dest='builder',
|
||||
action='store_false')
|
||||
# TODO(pabelanger): Deprecated flag, remove in the future.
|
||||
parser.add_argument('--build-workers', dest='build_workers',
|
||||
default=1, help='number of build workers',
|
||||
type=int)
|
||||
# TODO(pabelanger): Deprecated flag, remove in the future.
|
||||
parser.add_argument('--upload-workers', dest='upload_workers',
|
||||
default=4, help='number of upload workers',
|
||||
type=int)
|
||||
parser.add_argument('--no-deletes', action='store_true')
|
||||
parser.add_argument('--no-launches', action='store_true')
|
||||
parser.add_argument('--no-webapp', action='store_true')
|
||||
parser.add_argument('--version', dest='version', action='store_true',
|
||||
help='show version')
|
||||
self.args = parser.parse_args()
|
||||
|
||||
def exit_handler(self, signum, frame):
|
||||
self.pool.stop()
|
||||
if not self.args.no_webapp:
|
||||
self.webapp.stop()
|
||||
sys.exit(0)
|
||||
|
||||
def term_handler(self, signum, frame):
|
||||
os._exit(0)
|
||||
|
||||
def main(self):
|
||||
self.setup_logging()
|
||||
self.pool = nodepool.nodepool.NodePool(self.args.secure,
|
||||
self.args.config,
|
||||
self.args.no_deletes,
|
||||
self.args.no_launches)
|
||||
if self.args.builder:
|
||||
log.warning(
|
||||
"Note: nodepool no longer automatically builds images, "
|
||||
"please ensure the separate nodepool-builder process is "
|
||||
"running if you haven't already")
|
||||
else:
|
||||
log.warning(
|
||||
"--no-builder is deprecated and will be removed in the near "
|
||||
"future. Update your service scripts to avoid a breakage.")
|
||||
|
||||
if not self.args.no_webapp:
|
||||
self.webapp = nodepool.webapp.WebApp(self.pool)
|
||||
|
||||
signal.signal(signal.SIGINT, self.exit_handler)
|
||||
# For back compatibility:
|
||||
signal.signal(signal.SIGUSR1, self.exit_handler)
|
||||
|
||||
signal.signal(signal.SIGUSR2, nodepool.cmd.stack_dump_handler)
|
||||
signal.signal(signal.SIGTERM, self.term_handler)
|
||||
|
||||
self.pool.start()
|
||||
|
||||
if not self.args.no_webapp:
|
||||
self.webapp.start()
|
||||
|
||||
while True:
|
||||
signal.pause()
|
||||
|
||||
|
||||
def main():
|
||||
npd = NodePoolDaemon()
|
||||
npd.parse_arguments()
|
||||
|
||||
if npd.args.version:
|
||||
from nodepool.version import version_info as npd_version_info
|
||||
print "Nodepool version: %s" % npd_version_info.version_string()
|
||||
return(0)
|
||||
|
||||
pid = pid_file_module.TimeoutPIDLockFile(npd.args.pidfile, 10)
|
||||
if is_pidfile_stale(pid):
|
||||
pid.break_lock()
|
||||
|
||||
if npd.args.nodaemon:
|
||||
npd.main()
|
||||
else:
|
||||
with daemon.DaemonContext(pidfile=pid):
|
||||
npd.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
368
nodepool/config.py
Normal file → Executable file
368
nodepool/config.py
Normal file → Executable file
@ -16,114 +16,56 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os_client_config
|
||||
from six.moves import configparser as ConfigParser
|
||||
import time
|
||||
import yaml
|
||||
|
||||
import fakeprovider
|
||||
import zk
|
||||
|
||||
|
||||
class ConfigValue(object):
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, ConfigValue):
|
||||
if other.__dict__ == self.__dict__:
|
||||
return True
|
||||
return False
|
||||
from nodepool import zk
|
||||
from nodepool.driver import ConfigValue
|
||||
from nodepool.driver.fake.config import FakeProviderConfig
|
||||
from nodepool.driver.openstack.config import OpenStackProviderConfig
|
||||
|
||||
|
||||
class Config(ConfigValue):
|
||||
pass
|
||||
|
||||
|
||||
class Provider(ConfigValue):
|
||||
def __eq__(self, other):
|
||||
if (other.cloud_config != self.cloud_config or
|
||||
other.nodepool_id != self.nodepool_id or
|
||||
other.max_servers != self.max_servers or
|
||||
other.pool != self.pool or
|
||||
other.image_type != self.image_type or
|
||||
other.rate != self.rate or
|
||||
other.api_timeout != self.api_timeout or
|
||||
other.boot_timeout != self.boot_timeout or
|
||||
other.launch_timeout != self.launch_timeout or
|
||||
other.networks != self.networks or
|
||||
other.ipv6_preferred != self.ipv6_preferred or
|
||||
other.clean_floating_ips != self.clean_floating_ips or
|
||||
other.azs != self.azs):
|
||||
return False
|
||||
new_images = other.images
|
||||
old_images = self.images
|
||||
# Check if images have been added or removed
|
||||
if set(new_images.keys()) != set(old_images.keys()):
|
||||
return False
|
||||
# check if existing images have been updated
|
||||
for k in new_images:
|
||||
if (new_images[k].min_ram != old_images[k].min_ram or
|
||||
new_images[k].name_filter != old_images[k].name_filter or
|
||||
new_images[k].key_name != old_images[k].key_name or
|
||||
new_images[k].username != old_images[k].username or
|
||||
new_images[k].user_home != old_images[k].user_home or
|
||||
new_images[k].private_key != old_images[k].private_key or
|
||||
new_images[k].meta != old_images[k].meta or
|
||||
new_images[k].config_drive != old_images[k].config_drive):
|
||||
return False
|
||||
return True
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Provider %s>" % self.name
|
||||
|
||||
|
||||
class ProviderImage(ConfigValue):
|
||||
def __repr__(self):
|
||||
return "<ProviderImage %s>" % self.name
|
||||
|
||||
|
||||
class Target(ConfigValue):
|
||||
def __repr__(self):
|
||||
return "<Target %s>" % self.name
|
||||
|
||||
|
||||
class Label(ConfigValue):
|
||||
def __repr__(self):
|
||||
return "<Label %s>" % self.name
|
||||
|
||||
|
||||
class LabelProvider(ConfigValue):
|
||||
def __repr__(self):
|
||||
return "<LabelProvider %s>" % self.name
|
||||
|
||||
|
||||
class Cron(ConfigValue):
|
||||
def __repr__(self):
|
||||
return "<Cron %s>" % self.name
|
||||
|
||||
|
||||
class ZMQPublisher(ConfigValue):
|
||||
def __repr__(self):
|
||||
return "<ZMQPublisher %s>" % self.name
|
||||
|
||||
|
||||
class GearmanServer(ConfigValue):
|
||||
def __repr__(self):
|
||||
return "<GearmanServer %s>" % self.name
|
||||
|
||||
|
||||
class DiskImage(ConfigValue):
|
||||
def __eq__(self, other):
|
||||
if (other.name != self.name or
|
||||
other.elements != self.elements or
|
||||
other.release != self.release or
|
||||
other.rebuild_age != self.rebuild_age or
|
||||
other.env_vars != self.env_vars or
|
||||
other.image_types != self.image_types or
|
||||
other.pause != self.pause or
|
||||
other.username != self.username):
|
||||
return False
|
||||
return True
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return "<DiskImage %s>" % self.name
|
||||
|
||||
|
||||
class Network(ConfigValue):
|
||||
def __repr__(self):
|
||||
return "<Network name:%s id:%s>" % (self.name, self.id)
|
||||
def get_provider_config(provider):
|
||||
provider.setdefault('driver', 'openstack')
|
||||
# Ensure legacy configuration still works when using fake cloud
|
||||
if provider.get('name', '').startswith('fake'):
|
||||
provider['driver'] = 'fake'
|
||||
if provider['driver'] == 'fake':
|
||||
return FakeProviderConfig(provider)
|
||||
elif provider['driver'] == 'openstack':
|
||||
return OpenStackProviderConfig(provider)
|
||||
|
||||
|
||||
def loadConfig(config_path):
|
||||
def openConfig(path):
|
||||
retry = 3
|
||||
|
||||
# Since some nodepool code attempts to dynamically re-read its config
|
||||
@ -132,7 +74,7 @@ def loadConfig(config_path):
|
||||
# attempt to reload it.
|
||||
while True:
|
||||
try:
|
||||
config = yaml.load(open(config_path))
|
||||
config = yaml.load(open(path))
|
||||
break
|
||||
except IOError as e:
|
||||
if e.errno == 2:
|
||||
@ -142,48 +84,29 @@ def loadConfig(config_path):
|
||||
raise e
|
||||
if retry == 0:
|
||||
raise e
|
||||
return config
|
||||
|
||||
cloud_config = os_client_config.OpenStackConfig()
|
||||
|
||||
def loadConfig(config_path):
|
||||
config = openConfig(config_path)
|
||||
|
||||
# Reset the shared os_client_config instance
|
||||
OpenStackProviderConfig.os_client_config = None
|
||||
|
||||
newconfig = Config()
|
||||
newconfig.db = None
|
||||
newconfig.dburi = None
|
||||
newconfig.webapp = {
|
||||
'port': config.get('webapp', {}).get('port', 8005),
|
||||
'listen_address': config.get('webapp', {}).get('listen_address',
|
||||
'0.0.0.0')
|
||||
}
|
||||
newconfig.providers = {}
|
||||
newconfig.targets = {}
|
||||
newconfig.labels = {}
|
||||
newconfig.elementsdir = config.get('elements-dir')
|
||||
newconfig.imagesdir = config.get('images-dir')
|
||||
newconfig.dburi = None
|
||||
newconfig.provider_managers = {}
|
||||
newconfig.jenkins_managers = {}
|
||||
newconfig.zmq_publishers = {}
|
||||
newconfig.gearman_servers = {}
|
||||
newconfig.zookeeper_servers = {}
|
||||
newconfig.diskimages = {}
|
||||
newconfig.crons = {}
|
||||
|
||||
for name, default in [
|
||||
('cleanup', '* * * * *'),
|
||||
('check', '*/15 * * * *'),
|
||||
]:
|
||||
c = Cron()
|
||||
c.name = name
|
||||
newconfig.crons[c.name] = c
|
||||
c.job = None
|
||||
c.timespec = config.get('cron', {}).get(name, default)
|
||||
|
||||
for addr in config.get('zmq-publishers', []):
|
||||
z = ZMQPublisher()
|
||||
z.name = addr
|
||||
z.listener = None
|
||||
newconfig.zmq_publishers[z.name] = z
|
||||
|
||||
for server in config.get('gearman-servers', []):
|
||||
g = GearmanServer()
|
||||
g.host = server['host']
|
||||
g.port = server.get('port', 4730)
|
||||
g.name = g.host + '_' + str(g.port)
|
||||
newconfig.gearman_servers[g.name] = g
|
||||
|
||||
for server in config.get('zookeeper-servers', []):
|
||||
z = zk.ZooKeeperConnectionConfig(server['host'],
|
||||
@ -192,185 +115,54 @@ def loadConfig(config_path):
|
||||
name = z.host + '_' + str(z.port)
|
||||
newconfig.zookeeper_servers[name] = z
|
||||
|
||||
for provider in config.get('providers', []):
|
||||
p = Provider()
|
||||
p.name = provider['name']
|
||||
newconfig.providers[p.name] = p
|
||||
|
||||
cloud_kwargs = _cloudKwargsFromProvider(provider)
|
||||
p.cloud_config = _get_one_cloud(cloud_config, cloud_kwargs)
|
||||
p.nodepool_id = provider.get('nodepool-id', None)
|
||||
p.region_name = provider.get('region-name')
|
||||
p.max_servers = provider['max-servers']
|
||||
p.pool = provider.get('pool', None)
|
||||
p.rate = provider.get('rate', 1.0)
|
||||
p.api_timeout = provider.get('api-timeout')
|
||||
p.boot_timeout = provider.get('boot-timeout', 60)
|
||||
p.launch_timeout = provider.get('launch-timeout', 3600)
|
||||
p.networks = []
|
||||
for network in provider.get('networks', []):
|
||||
n = Network()
|
||||
p.networks.append(n)
|
||||
if 'net-id' in network:
|
||||
n.id = network['net-id']
|
||||
n.name = None
|
||||
elif 'net-label' in network:
|
||||
n.name = network['net-label']
|
||||
n.id = None
|
||||
else:
|
||||
n.name = network.get('name')
|
||||
n.id = None
|
||||
p.ipv6_preferred = provider.get('ipv6-preferred')
|
||||
p.clean_floating_ips = provider.get('clean-floating-ips')
|
||||
p.azs = provider.get('availability-zones')
|
||||
p.template_hostname = provider.get(
|
||||
'template-hostname',
|
||||
'template-{image.name}-{timestamp}'
|
||||
)
|
||||
p.image_type = provider.get(
|
||||
'image-type', p.cloud_config.config['image_format'])
|
||||
p.images = {}
|
||||
for image in provider['images']:
|
||||
i = ProviderImage()
|
||||
i.name = image['name']
|
||||
p.images[i.name] = i
|
||||
i.min_ram = image['min-ram']
|
||||
i.name_filter = image.get('name-filter', None)
|
||||
i.key_name = image.get('key-name', None)
|
||||
i.username = image.get('username', 'jenkins')
|
||||
i.user_home = image.get('user-home', '/home/jenkins')
|
||||
i.pause = bool(image.get('pause', False))
|
||||
i.private_key = image.get('private-key',
|
||||
'/var/lib/jenkins/.ssh/id_rsa')
|
||||
i.config_drive = image.get('config-drive', True)
|
||||
|
||||
# This dict is expanded and used as custom properties when
|
||||
# the image is uploaded.
|
||||
i.meta = image.get('meta', {})
|
||||
# 5 elements, and no key or value can be > 255 chars
|
||||
# per Nova API rules
|
||||
if i.meta:
|
||||
if len(i.meta) > 5 or \
|
||||
any([len(k) > 255 or len(v) > 255
|
||||
for k, v in i.meta.iteritems()]):
|
||||
# soft-fail
|
||||
#self.log.error("Invalid metadata for %s; ignored"
|
||||
# % i.name)
|
||||
i.meta = {}
|
||||
|
||||
if 'diskimages' in config:
|
||||
for diskimage in config['diskimages']:
|
||||
d = DiskImage()
|
||||
d.name = diskimage['name']
|
||||
newconfig.diskimages[d.name] = d
|
||||
if 'elements' in diskimage:
|
||||
d.elements = u' '.join(diskimage['elements'])
|
||||
else:
|
||||
d.elements = ''
|
||||
# must be a string, as it's passed as env-var to
|
||||
# d-i-b, but might be untyped in the yaml and
|
||||
# interpreted as a number (e.g. "21" for fedora)
|
||||
d.release = str(diskimage.get('release', ''))
|
||||
d.rebuild_age = int(diskimage.get('rebuild-age', 86400))
|
||||
d.env_vars = diskimage.get('env-vars', {})
|
||||
if not isinstance(d.env_vars, dict):
|
||||
#self.log.error("%s: ignoring env-vars; "
|
||||
# "should be a dict" % d.name)
|
||||
d.env_vars = {}
|
||||
d.image_types = set(diskimage.get('formats', []))
|
||||
d.pause = bool(diskimage.get('pause', False))
|
||||
# Do this after providers to build the image-types
|
||||
for provider in newconfig.providers.values():
|
||||
for image in provider.images.values():
|
||||
diskimage = newconfig.diskimages[image.name]
|
||||
diskimage.image_types.add(provider.image_type)
|
||||
for diskimage in config.get('diskimages', []):
|
||||
d = DiskImage()
|
||||
d.name = diskimage['name']
|
||||
newconfig.diskimages[d.name] = d
|
||||
if 'elements' in diskimage:
|
||||
d.elements = u' '.join(diskimage['elements'])
|
||||
else:
|
||||
d.elements = ''
|
||||
# must be a string, as it's passed as env-var to
|
||||
# d-i-b, but might be untyped in the yaml and
|
||||
# interpreted as a number (e.g. "21" for fedora)
|
||||
d.release = str(diskimage.get('release', ''))
|
||||
d.rebuild_age = int(diskimage.get('rebuild-age', 86400))
|
||||
d.env_vars = diskimage.get('env-vars', {})
|
||||
if not isinstance(d.env_vars, dict):
|
||||
d.env_vars = {}
|
||||
d.image_types = set(diskimage.get('formats', []))
|
||||
d.pause = bool(diskimage.get('pause', False))
|
||||
d.username = diskimage.get('username', 'zuul')
|
||||
|
||||
for label in config.get('labels', []):
|
||||
l = Label()
|
||||
l.name = label['name']
|
||||
newconfig.labels[l.name] = l
|
||||
l.image = label['image']
|
||||
l.max_ready_age = label.get('max-ready-age', 0)
|
||||
l.min_ready = label.get('min-ready', 2)
|
||||
l.subnodes = label.get('subnodes', 0)
|
||||
l.ready_script = label.get('ready-script')
|
||||
l.providers = {}
|
||||
for provider in label['providers']:
|
||||
p = LabelProvider()
|
||||
p.name = provider['name']
|
||||
l.providers[p.name] = p
|
||||
|
||||
for target in config.get('targets', []):
|
||||
t = Target()
|
||||
t.name = target['name']
|
||||
newconfig.targets[t.name] = t
|
||||
jenkins = target.get('jenkins', {})
|
||||
t.online = True
|
||||
t.rate = target.get('rate', 1.0)
|
||||
t.jenkins_test_job = jenkins.get('test-job')
|
||||
t.jenkins_url = None
|
||||
t.jenkins_user = None
|
||||
t.jenkins_apikey = None
|
||||
t.jenkins_credentials_id = None
|
||||
|
||||
t.assign_via_gearman = target.get('assign-via-gearman', False)
|
||||
|
||||
t.hostname = target.get(
|
||||
'hostname',
|
||||
'{label.name}-{provider.name}-{node_id}'
|
||||
)
|
||||
t.subnode_hostname = target.get(
|
||||
'subnode-hostname',
|
||||
'{label.name}-{provider.name}-{node_id}-{subnode_id}'
|
||||
)
|
||||
l.pools = []
|
||||
|
||||
for provider in config.get('providers', []):
|
||||
p = get_provider_config(provider)
|
||||
p.load(newconfig)
|
||||
newconfig.providers[p.name] = p
|
||||
return newconfig
|
||||
|
||||
|
||||
def loadSecureConfig(config, secure_config_path):
|
||||
secure = ConfigParser.ConfigParser()
|
||||
secure.readfp(open(secure_config_path))
|
||||
secure = openConfig(secure_config_path)
|
||||
if not secure: # empty file
|
||||
return
|
||||
|
||||
config.dburi = secure.get('database', 'dburi')
|
||||
# Eliminate any servers defined in the normal config
|
||||
if secure.get('zookeeper-servers', []):
|
||||
config.zookeeper_servers = {}
|
||||
|
||||
for target in config.targets.values():
|
||||
section_name = 'jenkins "%s"' % target.name
|
||||
if secure.has_section(section_name):
|
||||
target.jenkins_url = secure.get(section_name, 'url')
|
||||
target.jenkins_user = secure.get(section_name, 'user')
|
||||
target.jenkins_apikey = secure.get(section_name, 'apikey')
|
||||
|
||||
try:
|
||||
target.jenkins_credentials_id = secure.get(
|
||||
section_name, 'credentials')
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def _cloudKwargsFromProvider(provider):
|
||||
cloud_kwargs = {}
|
||||
for arg in ['region-name', 'api-timeout', 'cloud']:
|
||||
if arg in provider:
|
||||
cloud_kwargs[arg] = provider[arg]
|
||||
|
||||
# These are named from back when we only talked to Nova. They're
|
||||
# actually compute service related
|
||||
if 'service-type' in provider:
|
||||
cloud_kwargs['compute-service-type'] = provider['service-type']
|
||||
if 'service-name' in provider:
|
||||
cloud_kwargs['compute-service-name'] = provider['service-name']
|
||||
|
||||
auth_kwargs = {}
|
||||
for auth_key in (
|
||||
'username', 'password', 'auth-url', 'project-id', 'project-name'):
|
||||
if auth_key in provider:
|
||||
auth_kwargs[auth_key] = provider[auth_key]
|
||||
|
||||
cloud_kwargs['auth'] = auth_kwargs
|
||||
return cloud_kwargs
|
||||
|
||||
|
||||
def _get_one_cloud(cloud_config, cloud_kwargs):
|
||||
'''This is a function to allow for overriding it in tests.'''
|
||||
if cloud_kwargs.get('auth', {}).get('auth-url', '') == 'fake':
|
||||
return fakeprovider.fake_get_one_cloud(cloud_config, cloud_kwargs)
|
||||
return cloud_config.get_one_cloud(**cloud_kwargs)
|
||||
# TODO(Shrews): Support ZooKeeper auth
|
||||
for server in secure.get('zookeeper-servers', []):
|
||||
z = zk.ZooKeeperConnectionConfig(server['host'],
|
||||
server.get('port', 2181),
|
||||
server.get('chroot', None))
|
||||
name = z.host + '_' + str(z.port)
|
||||
config.zookeeper_servers[name] = z
|
||||
|
360
nodepool/driver/__init__.py
Normal file
360
nodepool/driver/__init__.py
Normal file
@ -0,0 +1,360 @@
|
||||
# Copyright (C) 2011-2014 OpenStack Foundation
|
||||
# Copyright (C) 2017 Red Hat
|
||||
#
|
||||
# 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 abc
|
||||
|
||||
import six
|
||||
|
||||
from nodepool import zk
|
||||
from nodepool import exceptions
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Provider(object):
|
||||
"""The Provider interface
|
||||
|
||||
The class or instance attribute **name** must be provided as a string.
|
||||
|
||||
"""
|
||||
@abc.abstractmethod
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def join(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def labelReady(self, name):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def cleanupNode(self, node_id):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def waitForNodeCleanup(self, node_id):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def cleanupLeakedResources(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def listNodes(self):
|
||||
pass
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class NodeRequestHandler(object):
|
||||
'''
|
||||
Class to process a single node request.
|
||||
|
||||
The PoolWorker thread will instantiate a class of this type for each
|
||||
node request that it pulls from ZooKeeper.
|
||||
|
||||
Subclasses are required to implement the run_handler method and the
|
||||
NodeLaunchManager to kick off any threads needed to satisfy the request.
|
||||
'''
|
||||
|
||||
def __init__(self, pw, request):
|
||||
'''
|
||||
:param PoolWorker pw: The parent PoolWorker object.
|
||||
:param NodeRequest request: The request to handle.
|
||||
'''
|
||||
self.pw = pw
|
||||
self.request = request
|
||||
self.launch_manager = None
|
||||
self.nodeset = []
|
||||
self.done = False
|
||||
self.paused = False
|
||||
self.launcher_id = self.pw.launcher_id
|
||||
|
||||
def _setFromPoolWorker(self):
|
||||
'''
|
||||
Set values that we pull from the parent PoolWorker.
|
||||
|
||||
We don't do this in __init__ because this class is re-entrant and we
|
||||
want the updated values.
|
||||
'''
|
||||
self.provider = self.pw.getProviderConfig()
|
||||
self.pool = self.pw.getPoolConfig()
|
||||
self.zk = self.pw.getZK()
|
||||
self.manager = self.pw.getProviderManager()
|
||||
|
||||
@property
|
||||
def alive_thread_count(self):
|
||||
if not self.launch_manager:
|
||||
return 0
|
||||
return self.launch_manager.alive_thread_count
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# Public methods
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
def unlockNodeSet(self, clear_allocation=False):
|
||||
'''
|
||||
Attempt unlocking all Nodes in the node set.
|
||||
|
||||
:param bool clear_allocation: If true, clears the node allocated_to
|
||||
attribute.
|
||||
'''
|
||||
for node in self.nodeset:
|
||||
if not node.lock:
|
||||
continue
|
||||
|
||||
if clear_allocation:
|
||||
node.allocated_to = None
|
||||
self.zk.storeNode(node)
|
||||
|
||||
try:
|
||||
self.zk.unlockNode(node)
|
||||
except Exception:
|
||||
self.log.exception("Error unlocking node:")
|
||||
self.log.debug("Unlocked node %s for request %s",
|
||||
node.id, self.request.id)
|
||||
|
||||
self.nodeset = []
|
||||
|
||||
def decline_request(self):
|
||||
self.request.declined_by.append(self.launcher_id)
|
||||
launchers = set(self.zk.getRegisteredLaunchers())
|
||||
if launchers.issubset(set(self.request.declined_by)):
|
||||
# All launchers have declined it
|
||||
self.log.debug("Failing declined node request %s",
|
||||
self.request.id)
|
||||
self.request.state = zk.FAILED
|
||||
else:
|
||||
self.request.state = zk.REQUESTED
|
||||
|
||||
def run(self):
|
||||
'''
|
||||
Execute node request handling.
|
||||
|
||||
This code is designed to be re-entrant. Because we can't always
|
||||
satisfy a request immediately (due to lack of provider resources), we
|
||||
need to be able to call run() repeatedly until the request can be
|
||||
fulfilled. The node set is saved and added to between calls.
|
||||
'''
|
||||
try:
|
||||
self.run_handler()
|
||||
except Exception:
|
||||
self.log.exception(
|
||||
"Declining node request %s due to exception in "
|
||||
"NodeRequestHandler:", self.request.id)
|
||||
self.decline_request()
|
||||
self.unlockNodeSet(clear_allocation=True)
|
||||
self.zk.storeNodeRequest(self.request)
|
||||
self.zk.unlockNodeRequest(self.request)
|
||||
self.done = True
|
||||
|
||||
def poll(self):
|
||||
'''
|
||||
Check if the request has been handled.
|
||||
|
||||
Once the request has been handled, the 'nodeset' attribute will be
|
||||
filled with the list of nodes assigned to the request, or it will be
|
||||
empty if the request could not be fulfilled.
|
||||
|
||||
:returns: True if we are done with the request, False otherwise.
|
||||
'''
|
||||
if self.paused:
|
||||
return False
|
||||
|
||||
if self.done:
|
||||
return True
|
||||
|
||||
if not self.launch_manager.poll():
|
||||
return False
|
||||
|
||||
# If the request has been pulled, unallocate the node set so other
|
||||
# requests can use them.
|
||||
if not self.zk.getNodeRequest(self.request.id):
|
||||
self.log.info("Node request %s disappeared", self.request.id)
|
||||
for node in self.nodeset:
|
||||
node.allocated_to = None
|
||||
self.zk.storeNode(node)
|
||||
self.unlockNodeSet()
|
||||
try:
|
||||
self.zk.unlockNodeRequest(self.request)
|
||||
except exceptions.ZKLockException:
|
||||
# If the lock object is invalid that is "ok" since we no
|
||||
# longer have a request either. Just do our best, log and
|
||||
# move on.
|
||||
self.log.debug("Request lock invalid for node request %s "
|
||||
"when attempting to clean up the lock",
|
||||
self.request.id)
|
||||
return True
|
||||
|
||||
if self.launch_manager.failed_nodes:
|
||||
self.log.debug("Declining node request %s because nodes failed",
|
||||
self.request.id)
|
||||
self.decline_request()
|
||||
else:
|
||||
# The assigned nodes must be added to the request in the order
|
||||
# in which they were requested.
|
||||
assigned = []
|
||||
for requested_type in self.request.node_types:
|
||||
for node in self.nodeset:
|
||||
if node.id in assigned:
|
||||
continue
|
||||
if node.type == requested_type:
|
||||
# Record node ID in the request
|
||||
self.request.nodes.append(node.id)
|
||||
assigned.append(node.id)
|
||||
|
||||
self.log.debug("Fulfilled node request %s",
|
||||
self.request.id)
|
||||
self.request.state = zk.FULFILLED
|
||||
|
||||
self.unlockNodeSet()
|
||||
self.zk.storeNodeRequest(self.request)
|
||||
self.zk.unlockNodeRequest(self.request)
|
||||
return True
|
||||
|
||||
@abc.abstractmethod
|
||||
def run_handler(self):
|
||||
pass
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class NodeLaunchManager(object):
|
||||
'''
|
||||
Handle launching multiple nodes in parallel.
|
||||
|
||||
Subclasses are required to implement the launch method.
|
||||
'''
|
||||
def __init__(self, zk, pool, provider_manager,
|
||||
requestor, retries):
|
||||
'''
|
||||
Initialize the launch manager.
|
||||
|
||||
:param ZooKeeper zk: A ZooKeeper object.
|
||||
:param ProviderPool pool: A config ProviderPool object.
|
||||
:param ProviderManager provider_manager: The manager object used to
|
||||
interact with the selected provider.
|
||||
:param str requestor: Identifier for the request originator.
|
||||
:param int retries: Number of times to retry failed launches.
|
||||
'''
|
||||
self._retries = retries
|
||||
self._nodes = []
|
||||
self._failed_nodes = []
|
||||
self._ready_nodes = []
|
||||
self._threads = []
|
||||
self._zk = zk
|
||||
self._pool = pool
|
||||
self._provider_manager = provider_manager
|
||||
self._requestor = requestor
|
||||
|
||||
@property
|
||||
def alive_thread_count(self):
|
||||
count = 0
|
||||
for t in self._threads:
|
||||
if t.isAlive():
|
||||
count += 1
|
||||
return count
|
||||
|
||||
@property
|
||||
def failed_nodes(self):
|
||||
return self._failed_nodes
|
||||
|
||||
@property
|
||||
def ready_nodes(self):
|
||||
return self._ready_nodes
|
||||
|
||||
def poll(self):
|
||||
'''
|
||||
Check if all launch requests have completed.
|
||||
|
||||
When all of the Node objects have reached a final state (READY or
|
||||
FAILED), we'll know all threads have finished the launch process.
|
||||
'''
|
||||
if not self._threads:
|
||||
return True
|
||||
|
||||
# Give the NodeLaunch threads time to finish.
|
||||
if self.alive_thread_count:
|
||||
return False
|
||||
|
||||
node_states = [node.state for node in self._nodes]
|
||||
|
||||
# NOTE: It very important that NodeLauncher always sets one of
|
||||
# these states, no matter what.
|
||||
if not all(s in (zk.READY, zk.FAILED) for s in node_states):
|
||||
return False
|
||||
|
||||
for node in self._nodes:
|
||||
if node.state == zk.READY:
|
||||
self._ready_nodes.append(node)
|
||||
else:
|
||||
self._failed_nodes.append(node)
|
||||
|
||||
return True
|
||||
|
||||
@abc.abstractmethod
|
||||
def launch(self, node):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigValue(object):
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, ConfigValue):
|
||||
if other.__dict__ == self.__dict__:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
|
||||
class Driver(ConfigValue):
|
||||
pass
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ProviderConfig(ConfigValue):
|
||||
"""The Provider config interface
|
||||
|
||||
The class or instance attribute **name** must be provided as a string.
|
||||
|
||||
"""
|
||||
def __init__(self, provider):
|
||||
self.name = provider['name']
|
||||
self.provider = provider
|
||||
self.driver = Driver()
|
||||
self.driver.name = provider.get('driver', 'openstack')
|
||||
self.max_concurrency = provider.get('max-concurrency', -1)
|
||||
self.driver.manage_images = False
|
||||
|
||||
def __repr__(self):
|
||||
return "<Provider %s>" % self.name
|
||||
|
||||
@abc.abstractmethod
|
||||
def __eq__(self, other):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def load(self, newconfig):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_schema(self):
|
||||
pass
|
0
nodepool/driver/fake/__init__.py
Normal file
0
nodepool/driver/fake/__init__.py
Normal file
22
nodepool/driver/fake/config.py
Normal file
22
nodepool/driver/fake/config.py
Normal file
@ -0,0 +1,22 @@
|
||||
# Copyright 2017 Red Hat
|
||||
#
|
||||
# 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 nodepool.driver.openstack.config import OpenStackProviderConfig
|
||||
|
||||
|
||||
class FakeProviderConfig(OpenStackProviderConfig):
|
||||
def _cloudKwargs(self):
|
||||
cloud_kwargs = super(FakeProviderConfig, self)._cloudKwargs()
|
||||
cloud_kwargs['validate'] = False
|
||||
return cloud_kwargs
|
19
nodepool/driver/fake/handler.py
Normal file
19
nodepool/driver/fake/handler.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Copyright 2017 Red Hat
|
||||
#
|
||||
# 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 nodepool.driver.openstack.handler import OpenStackNodeRequestHandler
|
||||
|
||||
|
||||
class FakeNodeRequestHandler(OpenStackNodeRequestHandler):
|
||||
launcher_id = "Fake"
|
@ -1,35 +1,35 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright (C) 2011-2013 OpenStack Foundation
|
||||
#
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# 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
|
||||
#
|
||||
# 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
|
||||
# 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.
|
||||
# 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 StringIO
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from jenkins import JenkinsException
|
||||
import shade
|
||||
|
||||
import exceptions
|
||||
from nodepool import exceptions
|
||||
from nodepool.driver.openstack.provider import OpenStackProvider
|
||||
|
||||
|
||||
class Dummy(object):
|
||||
IMAGE = 'Image'
|
||||
INSTANCE = 'Instance'
|
||||
FLAVOR = 'Flavor'
|
||||
LOCATION = 'Server.Location'
|
||||
|
||||
def __init__(self, kind, **kw):
|
||||
self.__kind = kind
|
||||
@ -40,6 +40,9 @@ class Dummy(object):
|
||||
if self.should_fail:
|
||||
raise shade.OpenStackCloudException('This image has '
|
||||
'SHOULD_FAIL set to True.')
|
||||
if self.over_quota:
|
||||
raise shade.exc.OpenStackCloudHTTPError(
|
||||
'Quota exceeded for something', 403)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
@ -63,16 +66,15 @@ class Dummy(object):
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
def fake_get_one_cloud(cloud_config, cloud_kwargs):
|
||||
cloud_kwargs['validate'] = False
|
||||
cloud_kwargs['image_format'] = 'qcow2'
|
||||
return cloud_config.get_one_cloud(**cloud_kwargs)
|
||||
def get_fake_quota():
|
||||
return 100, 20, 1000000
|
||||
|
||||
|
||||
class FakeOpenStackCloud(object):
|
||||
log = logging.getLogger("nodepool.FakeOpenStackCloud")
|
||||
|
||||
def __init__(self, images=None, networks=None):
|
||||
self.pause_creates = False
|
||||
self._image_list = images
|
||||
if self._image_list is None:
|
||||
self._image_list = [
|
||||
@ -87,13 +89,18 @@ class FakeOpenStackCloud(object):
|
||||
networks = [dict(id='fake-public-network-uuid',
|
||||
name='fake-public-network-name'),
|
||||
dict(id='fake-private-network-uuid',
|
||||
name='fake-private-network-name')]
|
||||
name='fake-private-network-name'),
|
||||
dict(id='fake-ipv6-network-uuid',
|
||||
name='fake-ipv6-network-name')]
|
||||
self.networks = networks
|
||||
self._flavor_list = [
|
||||
Dummy(Dummy.FLAVOR, id='f1', ram=8192, name='Fake Flavor'),
|
||||
Dummy(Dummy.FLAVOR, id='f2', ram=8192, name='Unreal Flavor'),
|
||||
Dummy(Dummy.FLAVOR, id='f1', ram=8192, name='Fake Flavor',
|
||||
vcpus=4),
|
||||
Dummy(Dummy.FLAVOR, id='f2', ram=8192, name='Unreal Flavor',
|
||||
vcpus=4),
|
||||
]
|
||||
self._server_list = []
|
||||
self.max_cores, self.max_instances, self.max_ram = get_fake_quota()
|
||||
|
||||
def _get(self, name_or_id, instance_list):
|
||||
self.log.debug("Get %s in %s" % (name_or_id, repr(instance_list)))
|
||||
@ -103,19 +110,20 @@ class FakeOpenStackCloud(object):
|
||||
return None
|
||||
|
||||
def get_network(self, name_or_id, filters=None):
|
||||
return dict(id='fake-network-uuid',
|
||||
name='fake-network-name')
|
||||
for net in self.networks:
|
||||
if net['id'] == name_or_id or net['name'] == name_or_id:
|
||||
return net
|
||||
return self.networks[0]
|
||||
|
||||
def _create(
|
||||
self, instance_list, instance_type=Dummy.INSTANCE,
|
||||
done_status='ACTIVE', **kw):
|
||||
def _create(self, instance_list, instance_type=Dummy.INSTANCE,
|
||||
done_status='ACTIVE', max_quota=-1, **kw):
|
||||
should_fail = kw.get('SHOULD_FAIL', '').lower() == 'true'
|
||||
nics = kw.get('nics', [])
|
||||
addresses = None
|
||||
# if keyword 'ipv6-uuid' is found in provider config,
|
||||
# ipv6 address will be available in public addr dict.
|
||||
for nic in nics:
|
||||
if 'ipv6-uuid' not in nic['net-id']:
|
||||
if nic['net-id'] != 'fake-ipv6-network-uuid':
|
||||
continue
|
||||
addresses = dict(
|
||||
public=[dict(version=4, addr='fake'),
|
||||
@ -125,6 +133,7 @@ class FakeOpenStackCloud(object):
|
||||
public_v6 = 'fake_v6'
|
||||
public_v4 = 'fake'
|
||||
private_v4 = 'fake'
|
||||
interface_ip = 'fake_v6'
|
||||
break
|
||||
if not addresses:
|
||||
addresses = dict(
|
||||
@ -134,6 +143,12 @@ class FakeOpenStackCloud(object):
|
||||
public_v6 = ''
|
||||
public_v4 = 'fake'
|
||||
private_v4 = 'fake'
|
||||
interface_ip = 'fake'
|
||||
over_quota = False
|
||||
if (instance_type == Dummy.INSTANCE and
|
||||
self.max_instances > -1 and
|
||||
len(instance_list) >= self.max_instances):
|
||||
over_quota = True
|
||||
|
||||
s = Dummy(instance_type,
|
||||
id=uuid.uuid4().hex,
|
||||
@ -144,10 +159,14 @@ class FakeOpenStackCloud(object):
|
||||
public_v4=public_v4,
|
||||
public_v6=public_v6,
|
||||
private_v4=private_v4,
|
||||
interface_ip=interface_ip,
|
||||
location=Dummy(Dummy.LOCATION, zone=kw.get('az')),
|
||||
metadata=kw.get('meta', {}),
|
||||
manager=self,
|
||||
key_name=kw.get('key_name', None),
|
||||
should_fail=should_fail)
|
||||
should_fail=should_fail,
|
||||
over_quota=over_quota,
|
||||
event=threading.Event())
|
||||
instance_list.append(s)
|
||||
t = threading.Thread(target=self._finish,
|
||||
name='FakeProvider create',
|
||||
@ -166,7 +185,13 @@ class FakeOpenStackCloud(object):
|
||||
self.log.debug("Deleted from %s" % (repr(instance_list),))
|
||||
|
||||
def _finish(self, obj, delay, status):
|
||||
time.sleep(delay)
|
||||
self.log.debug("Pause creates %s", self.pause_creates)
|
||||
if self.pause_creates:
|
||||
self.log.debug("Pausing")
|
||||
obj.event.wait()
|
||||
self.log.debug("Continuing")
|
||||
else:
|
||||
time.sleep(delay)
|
||||
obj.status = status
|
||||
|
||||
def create_image(self, **kwargs):
|
||||
@ -198,6 +223,7 @@ class FakeOpenStackCloud(object):
|
||||
server.public_v4 = 'fake'
|
||||
server.public_v6 = 'fake'
|
||||
server.private_v4 = 'fake'
|
||||
server.interface_ip = 'fake'
|
||||
return server
|
||||
|
||||
def create_server(self, **kw):
|
||||
@ -207,8 +233,18 @@ class FakeOpenStackCloud(object):
|
||||
result = self._get(name_or_id, self._server_list)
|
||||
return result
|
||||
|
||||
def _clean_floating_ip(self, server):
|
||||
server.public_v4 = ''
|
||||
server.public_v6 = ''
|
||||
server.interface_ip = server.private_v4
|
||||
return server
|
||||
|
||||
def wait_for_server(self, server, **kwargs):
|
||||
server.status = 'ACTIVE'
|
||||
while server.status == 'BUILD':
|
||||
time.sleep(0.1)
|
||||
auto_ip = kwargs.get('auto_ip')
|
||||
if not auto_ip:
|
||||
server = self._clean_floating_ip(server)
|
||||
return server
|
||||
|
||||
def list_servers(self):
|
||||
@ -217,8 +253,19 @@ class FakeOpenStackCloud(object):
|
||||
def delete_server(self, name_or_id, delete_ips=True):
|
||||
self._delete(name_or_id, self._server_list)
|
||||
|
||||
def list_networks(self):
|
||||
return dict(networks=self.networks)
|
||||
def list_availability_zone_names(self):
|
||||
return ['fake-az1', 'fake-az2']
|
||||
|
||||
def get_compute_limits(self):
|
||||
return Dummy(
|
||||
'limits',
|
||||
max_total_cores=self.max_cores,
|
||||
max_total_instances=self.max_instances,
|
||||
max_total_ram_size=self.max_ram,
|
||||
total_cores_used=4 * len(self._server_list),
|
||||
total_instances_used=len(self._server_list),
|
||||
total_ram_used=8192 * len(self._server_list)
|
||||
)
|
||||
|
||||
|
||||
class FakeUploadFailCloud(FakeOpenStackCloud):
|
||||
@ -239,79 +286,17 @@ class FakeUploadFailCloud(FakeOpenStackCloud):
|
||||
return super(FakeUploadFailCloud, self).create_image(**kwargs)
|
||||
|
||||
|
||||
class FakeFile(StringIO.StringIO):
|
||||
def __init__(self, path):
|
||||
StringIO.StringIO.__init__(self)
|
||||
self.__path = path
|
||||
class FakeProvider(OpenStackProvider):
|
||||
def __init__(self, provider, use_taskmanager):
|
||||
self.createServer_fails = 0
|
||||
self.__client = FakeOpenStackCloud()
|
||||
super(FakeProvider, self).__init__(provider, use_taskmanager)
|
||||
|
||||
def close(self):
|
||||
print "Wrote to %s:" % self.__path
|
||||
print self.getvalue()
|
||||
StringIO.StringIO.close(self)
|
||||
def _getClient(self):
|
||||
return self.__client
|
||||
|
||||
|
||||
class FakeSFTPClient(object):
|
||||
def open(self, path, mode):
|
||||
return FakeFile(path)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
class FakeSSHClient(object):
|
||||
def __init__(self):
|
||||
self.client = self
|
||||
|
||||
def ssh(self, description, cmd, output=False):
|
||||
return True
|
||||
|
||||
def scp(self, src, dest):
|
||||
return True
|
||||
|
||||
def open_sftp(self):
|
||||
return FakeSFTPClient()
|
||||
|
||||
|
||||
class FakeJenkins(object):
|
||||
def __init__(self, user):
|
||||
self._nodes = {}
|
||||
self.quiet = False
|
||||
self.down = False
|
||||
if user == 'quiet':
|
||||
self.quiet = True
|
||||
if user == 'down':
|
||||
self.down = True
|
||||
|
||||
def node_exists(self, name):
|
||||
return name in self._nodes
|
||||
|
||||
def create_node(self, name, **kw):
|
||||
self._nodes[name] = kw
|
||||
|
||||
def delete_node(self, name):
|
||||
del self._nodes[name]
|
||||
|
||||
def get_info(self):
|
||||
if self.down:
|
||||
raise JenkinsException("Jenkins is down")
|
||||
d = {u'assignedLabels': [{}],
|
||||
u'description': None,
|
||||
u'jobs': [{u'color': u'red',
|
||||
u'name': u'test-job',
|
||||
u'url': u'https://jenkins.example.com/job/test-job/'}],
|
||||
u'mode': u'NORMAL',
|
||||
u'nodeDescription': u'the master Jenkins node',
|
||||
u'nodeName': u'',
|
||||
u'numExecutors': 1,
|
||||
u'overallLoad': {},
|
||||
u'primaryView': {u'name': u'Overview',
|
||||
u'url': u'https://jenkins.example.com/'},
|
||||
u'quietingDown': self.quiet,
|
||||
u'slaveAgentPort': 8090,
|
||||
u'unlabeledLoad': {},
|
||||
u'useCrumbs': False,
|
||||
u'useSecurity': True,
|
||||
u'views': [
|
||||
{u'name': u'test-view',
|
||||
u'url': u'https://jenkins.example.com/view/test-view/'}]}
|
||||
return d
|
||||
def createServer(self, *args, **kwargs):
|
||||
while self.createServer_fails:
|
||||
self.createServer_fails -= 1
|
||||
raise Exception("Expected createServer exception")
|
||||
return super(FakeProvider, self).createServer(*args, **kwargs)
|
0
nodepool/driver/openstack/__init__.py
Normal file
0
nodepool/driver/openstack/__init__.py
Normal file
272
nodepool/driver/openstack/config.py
Normal file
272
nodepool/driver/openstack/config.py
Normal file
@ -0,0 +1,272 @@
|
||||
# Copyright (C) 2011-2013 OpenStack Foundation
|
||||
#
|
||||
# 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 os_client_config
|
||||
import voluptuous as v
|
||||
|
||||
from nodepool.driver import ProviderConfig
|
||||
from nodepool.driver import ConfigValue
|
||||
|
||||
|
||||
class ProviderDiskImage(ConfigValue):
|
||||
def __repr__(self):
|
||||
return "<ProviderDiskImage %s>" % self.name
|
||||
|
||||
|
||||
class ProviderCloudImage(ConfigValue):
|
||||
def __repr__(self):
|
||||
return "<ProviderCloudImage %s>" % self.name
|
||||
|
||||
@property
|
||||
def external(self):
|
||||
'''External identifier to pass to the cloud.'''
|
||||
if self.image_id:
|
||||
return dict(id=self.image_id)
|
||||
else:
|
||||
return self.image_name or self.name
|
||||
|
||||
@property
|
||||
def external_name(self):
|
||||
'''Human readable version of external.'''
|
||||
return self.image_id or self.image_name or self.name
|
||||
|
||||
|
||||
class ProviderLabel(ConfigValue):
|
||||
def __eq__(self, other):
|
||||
if (other.diskimage != self.diskimage or
|
||||
other.cloud_image != self.cloud_image or
|
||||
other.min_ram != self.min_ram or
|
||||
other.flavor_name != self.flavor_name or
|
||||
other.key_name != self.key_name):
|
||||
return False
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return "<ProviderLabel %s>" % self.name
|
||||
|
||||
|
||||
class ProviderPool(ConfigValue):
|
||||
def __eq__(self, other):
|
||||
if (other.labels != self.labels or
|
||||
other.max_cores != self.max_cores or
|
||||
other.max_servers != self.max_servers or
|
||||
other.max_ram != self.max_ram or
|
||||
other.azs != self.azs or
|
||||
other.networks != self.networks):
|
||||
return False
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return "<ProviderPool %s>" % self.name
|
||||
|
||||
|
||||
class OpenStackProviderConfig(ProviderConfig):
|
||||
os_client_config = None
|
||||
|
||||
def __eq__(self, other):
|
||||
if (other.cloud_config != self.cloud_config or
|
||||
other.pools != self.pools or
|
||||
other.image_type != self.image_type or
|
||||
other.rate != self.rate or
|
||||
other.boot_timeout != self.boot_timeout or
|
||||
other.launch_timeout != self.launch_timeout or
|
||||
other.clean_floating_ips != self.clean_floating_ips or
|
||||
other.max_concurrency != self.max_concurrency or
|
||||
other.diskimages != self.diskimages):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _cloudKwargs(self):
|
||||
cloud_kwargs = {}
|
||||
for arg in ['region-name', 'cloud']:
|
||||
if arg in self.provider:
|
||||
cloud_kwargs[arg] = self.provider[arg]
|
||||
return cloud_kwargs
|
||||
|
||||
def load(self, config):
|
||||
if OpenStackProviderConfig.os_client_config is None:
|
||||
OpenStackProviderConfig.os_client_config = \
|
||||
os_client_config.OpenStackConfig()
|
||||
cloud_kwargs = self._cloudKwargs()
|
||||
self.cloud_config = self.os_client_config.get_one_cloud(**cloud_kwargs)
|
||||
|
||||
self.image_type = self.cloud_config.config['image_format']
|
||||
self.driver.manage_images = True
|
||||
self.region_name = self.provider.get('region-name')
|
||||
self.rate = self.provider.get('rate', 1.0)
|
||||
self.boot_timeout = self.provider.get('boot-timeout', 60)
|
||||
self.launch_timeout = self.provider.get('launch-timeout', 3600)
|
||||
self.launch_retries = self.provider.get('launch-retries', 3)
|
||||
self.clean_floating_ips = self.provider.get('clean-floating-ips')
|
||||
self.hostname_format = self.provider.get(
|
||||
'hostname-format',
|
||||
'{label.name}-{provider.name}-{node.id}'
|
||||
)
|
||||
self.image_name_format = self.provider.get(
|
||||
'image-name-format',
|
||||
'{image_name}-{timestamp}'
|
||||
)
|
||||
self.diskimages = {}
|
||||
for image in self.provider.get('diskimages', []):
|
||||
i = ProviderDiskImage()
|
||||
i.name = image['name']
|
||||
self.diskimages[i.name] = i
|
||||
diskimage = config.diskimages[i.name]
|
||||
diskimage.image_types.add(self.image_type)
|
||||
i.pause = bool(image.get('pause', False))
|
||||
i.config_drive = image.get('config-drive', None)
|
||||
i.connection_type = image.get('connection-type', 'ssh')
|
||||
|
||||
# This dict is expanded and used as custom properties when
|
||||
# the image is uploaded.
|
||||
i.meta = image.get('meta', {})
|
||||
# 5 elements, and no key or value can be > 255 chars
|
||||
# per Nova API rules
|
||||
if i.meta:
|
||||
if len(i.meta) > 5 or \
|
||||
any([len(k) > 255 or len(v) > 255
|
||||
for k, v in i.meta.items()]):
|
||||
# soft-fail
|
||||
# self.log.error("Invalid metadata for %s; ignored"
|
||||
# % i.name)
|
||||
i.meta = {}
|
||||
|
||||
self.cloud_images = {}
|
||||
for image in self.provider.get('cloud-images', []):
|
||||
i = ProviderCloudImage()
|
||||
i.name = image['name']
|
||||
i.config_drive = image.get('config-drive', None)
|
||||
i.image_id = image.get('image-id', None)
|
||||
i.image_name = image.get('image-name', None)
|
||||
i.username = image.get('username', None)
|
||||
i.connection_type = image.get('connection-type', 'ssh')
|
||||
self.cloud_images[i.name] = i
|
||||
|
||||
self.pools = {}
|
||||
for pool in self.provider.get('pools', []):
|
||||
pp = ProviderPool()
|
||||
pp.name = pool['name']
|
||||
pp.provider = self
|
||||
self.pools[pp.name] = pp
|
||||
pp.max_cores = pool.get('max-cores', None)
|
||||
pp.max_servers = pool.get('max-servers', None)
|
||||
pp.max_ram = pool.get('max-ram', None)
|
||||
pp.azs = pool.get('availability-zones')
|
||||
pp.networks = pool.get('networks', [])
|
||||
pp.auto_floating_ip = bool(pool.get('auto-floating-ip', True))
|
||||
pp.labels = {}
|
||||
for label in pool.get('labels', []):
|
||||
pl = ProviderLabel()
|
||||
pl.name = label['name']
|
||||
pl.pool = pp
|
||||
pp.labels[pl.name] = pl
|
||||
diskimage = label.get('diskimage', None)
|
||||
if diskimage:
|
||||
pl.diskimage = config.diskimages[diskimage]
|
||||
else:
|
||||
pl.diskimage = None
|
||||
cloud_image_name = label.get('cloud-image', None)
|
||||
if cloud_image_name:
|
||||
cloud_image = self.cloud_images.get(cloud_image_name, None)
|
||||
if not cloud_image:
|
||||
raise ValueError(
|
||||
"cloud-image %s does not exist in provider %s"
|
||||
" but is referenced in label %s" %
|
||||
(cloud_image_name, self.name, pl.name))
|
||||
else:
|
||||
cloud_image = None
|
||||
pl.cloud_image = cloud_image
|
||||
pl.min_ram = label.get('min-ram', 0)
|
||||
pl.flavor_name = label.get('flavor-name', None)
|
||||
pl.key_name = label.get('key-name')
|
||||
pl.console_log = label.get('console-log', False)
|
||||
pl.boot_from_volume = bool(label.get('boot-from-volume',
|
||||
False))
|
||||
pl.volume_size = label.get('volume-size', 50)
|
||||
|
||||
top_label = config.labels[pl.name]
|
||||
top_label.pools.append(pp)
|
||||
|
||||
def get_schema(self):
|
||||
provider_diskimage = {
|
||||
'name': str,
|
||||
'pause': bool,
|
||||
'meta': dict,
|
||||
'config-drive': bool,
|
||||
'connection-type': str,
|
||||
}
|
||||
|
||||
provider_cloud_images = {
|
||||
'name': str,
|
||||
'config-drive': bool,
|
||||
'connection-type': str,
|
||||
v.Exclusive('image-id', 'cloud-image-name-or-id'): str,
|
||||
v.Exclusive('image-name', 'cloud-image-name-or-id'): str,
|
||||
'username': str,
|
||||
}
|
||||
|
||||
pool_label_main = {
|
||||
v.Required('name'): str,
|
||||
v.Exclusive('diskimage', 'label-image'): str,
|
||||
v.Exclusive('cloud-image', 'label-image'): str,
|
||||
'min-ram': int,
|
||||
'flavor-name': str,
|
||||
'key-name': str,
|
||||
'console-log': bool,
|
||||
'boot-from-volume': bool,
|
||||
'volume-size': int,
|
||||
}
|
||||
|
||||
label_min_ram = v.Schema({v.Required('min-ram'): int}, extra=True)
|
||||
|
||||
label_flavor_name = v.Schema({v.Required('flavor-name'): str},
|
||||
extra=True)
|
||||
|
||||
label_diskimage = v.Schema({v.Required('diskimage'): str}, extra=True)
|
||||
|
||||
label_cloud_image = v.Schema({v.Required('cloud-image'): str},
|
||||
extra=True)
|
||||
|
||||
pool_label = v.All(pool_label_main,
|
||||
v.Any(label_min_ram, label_flavor_name),
|
||||
v.Any(label_diskimage, label_cloud_image))
|
||||
|
||||
pool = {
|
||||
'name': str,
|
||||
'networks': [str],
|
||||
'auto-floating-ip': bool,
|
||||
'max-cores': int,
|
||||
'max-servers': int,
|
||||
'max-ram': int,
|
||||
'labels': [pool_label],
|
||||
'availability-zones': [str],
|
||||
}
|
||||
|
||||
return v.Schema({
|
||||
'region-name': str,
|
||||
v.Required('cloud'): str,
|
||||
'boot-timeout': int,
|
||||
'launch-timeout': int,
|
||||
'launch-retries': int,
|
||||
'nodepool-id': str,
|
||||
'rate': float,
|
||||
'hostname-format': str,
|
||||
'image-name-format': str,
|
||||
'clean-floating-ips': bool,
|
||||
'pools': [pool],
|
||||
'diskimages': [provider_diskimage],
|
||||
'cloud-images': [provider_cloud_images],
|
||||
})
|
586
nodepool/driver/openstack/handler.py
Normal file
586
nodepool/driver/openstack/handler.py
Normal file
@ -0,0 +1,586 @@
|
||||
# Copyright (C) 2011-2014 OpenStack Foundation
|
||||
# Copyright 2017 Red Hat
|
||||
#
|
||||
# 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 collections
|
||||
import logging
|
||||
import math
|
||||
import pprint
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
|
||||
from nodepool import exceptions
|
||||
from nodepool import nodeutils as utils
|
||||
from nodepool import stats
|
||||
from nodepool import zk
|
||||
from nodepool.driver import NodeLaunchManager
|
||||
from nodepool.driver import NodeRequestHandler
|
||||
from nodepool.driver.openstack.provider import QuotaInformation
|
||||
|
||||
|
||||
class NodeLauncher(threading.Thread, stats.StatsReporter):
|
||||
log = logging.getLogger("nodepool.driver.openstack."
|
||||
"NodeLauncher")
|
||||
|
||||
def __init__(self, zk, provider_label, provider_manager, requestor,
|
||||
node, retries):
|
||||
'''
|
||||
Initialize the launcher.
|
||||
|
||||
:param ZooKeeper zk: A ZooKeeper object.
|
||||
:param ProviderLabel provider: A config ProviderLabel object.
|
||||
:param ProviderManager provider_manager: The manager object used to
|
||||
interact with the selected provider.
|
||||
:param str requestor: Identifier for the request originator.
|
||||
:param Node node: The node object.
|
||||
:param int retries: Number of times to retry failed launches.
|
||||
'''
|
||||
threading.Thread.__init__(self, name="NodeLauncher-%s" % node.id)
|
||||
stats.StatsReporter.__init__(self)
|
||||
self.log = logging.getLogger("nodepool.NodeLauncher-%s" % node.id)
|
||||
self._zk = zk
|
||||
self._label = provider_label
|
||||
self._provider_manager = provider_manager
|
||||
self._node = node
|
||||
self._retries = retries
|
||||
self._image_name = None
|
||||
self._requestor = requestor
|
||||
|
||||
self._pool = self._label.pool
|
||||
self._provider_config = self._pool.provider
|
||||
if self._label.diskimage:
|
||||
self._diskimage = self._provider_config.diskimages[
|
||||
self._label.diskimage.name]
|
||||
else:
|
||||
self._diskimage = None
|
||||
|
||||
def logConsole(self, server_id, hostname):
|
||||
if not self._label.console_log:
|
||||
return
|
||||
console = self._provider_manager.getServerConsole(server_id)
|
||||
if console:
|
||||
self.log.debug('Console log from hostname %s:' % hostname)
|
||||
for line in console.splitlines():
|
||||
self.log.debug(line.rstrip())
|
||||
|
||||
def _launchNode(self):
|
||||
if self._label.diskimage:
|
||||
# launch using diskimage
|
||||
cloud_image = self._zk.getMostRecentImageUpload(
|
||||
self._diskimage.name, self._provider_config.name)
|
||||
|
||||
if not cloud_image:
|
||||
raise exceptions.LaunchNodepoolException(
|
||||
"Unable to find current cloud image %s in %s" %
|
||||
(self._diskimage.name, self._provider_config.name)
|
||||
)
|
||||
|
||||
config_drive = self._diskimage.config_drive
|
||||
image_external = dict(id=cloud_image.external_id)
|
||||
image_id = "{path}/{upload_id}".format(
|
||||
path=self._zk._imageUploadPath(cloud_image.image_name,
|
||||
cloud_image.build_id,
|
||||
cloud_image.provider_name),
|
||||
upload_id=cloud_image.id)
|
||||
image_name = self._diskimage.name
|
||||
username = cloud_image.username
|
||||
connection_type = self._diskimage.connection_type
|
||||
|
||||
else:
|
||||
# launch using unmanaged cloud image
|
||||
config_drive = self._label.cloud_image.config_drive
|
||||
|
||||
image_external = self._label.cloud_image.external
|
||||
image_id = self._label.cloud_image.name
|
||||
image_name = self._label.cloud_image.name
|
||||
username = self._label.cloud_image.username
|
||||
connection_type = self._label.cloud_image.connection_type
|
||||
|
||||
hostname = self._provider_config.hostname_format.format(
|
||||
label=self._label, provider=self._provider_config, node=self._node
|
||||
)
|
||||
|
||||
self.log.info("Creating server with hostname %s in %s from image %s "
|
||||
"for node id: %s" % (hostname,
|
||||
self._provider_config.name,
|
||||
image_name,
|
||||
self._node.id))
|
||||
|
||||
# NOTE: We store the node ID in the server metadata to use for leaked
|
||||
# instance detection. We cannot use the external server ID for this
|
||||
# because that isn't available in ZooKeeper until after the server is
|
||||
# active, which could cause a race in leak detection.
|
||||
|
||||
server = self._provider_manager.createServer(
|
||||
hostname,
|
||||
image=image_external,
|
||||
min_ram=self._label.min_ram,
|
||||
flavor_name=self._label.flavor_name,
|
||||
key_name=self._label.key_name,
|
||||
az=self._node.az,
|
||||
config_drive=config_drive,
|
||||
nodepool_node_id=self._node.id,
|
||||
nodepool_node_label=self._node.type,
|
||||
nodepool_image_name=image_name,
|
||||
networks=self._pool.networks,
|
||||
boot_from_volume=self._label.boot_from_volume,
|
||||
volume_size=self._label.volume_size)
|
||||
|
||||
self._node.external_id = server.id
|
||||
self._node.hostname = hostname
|
||||
self._node.image_id = image_id
|
||||
if username:
|
||||
self._node.username = username
|
||||
self._node.connection_type = connection_type
|
||||
|
||||
# Checkpoint save the updated node info
|
||||
self._zk.storeNode(self._node)
|
||||
|
||||
self.log.debug("Waiting for server %s for node id: %s" %
|
||||
(server.id, self._node.id))
|
||||
server = self._provider_manager.waitForServer(
|
||||
server, self._provider_config.launch_timeout,
|
||||
auto_ip=self._pool.auto_floating_ip)
|
||||
|
||||
if server.status != 'ACTIVE':
|
||||
raise exceptions.LaunchStatusException("Server %s for node id: %s "
|
||||
"status: %s" %
|
||||
(server.id, self._node.id,
|
||||
server.status))
|
||||
|
||||
# If we didn't specify an AZ, set it to the one chosen by Nova.
|
||||
# Do this after we are done waiting since AZ may not be available
|
||||
# immediately after the create request.
|
||||
if not self._node.az:
|
||||
self._node.az = server.location.zone
|
||||
|
||||
interface_ip = server.interface_ip
|
||||
if not interface_ip:
|
||||
self.log.debug(
|
||||
"Server data for failed IP: %s" % pprint.pformat(
|
||||
server))
|
||||
raise exceptions.LaunchNetworkException(
|
||||
"Unable to find public IP of server")
|
||||
|
||||
self._node.interface_ip = interface_ip
|
||||
self._node.public_ipv4 = server.public_v4
|
||||
self._node.public_ipv6 = server.public_v6
|
||||
self._node.private_ipv4 = server.private_v4
|
||||
# devstack-gate multi-node depends on private_v4 being populated
|
||||
# with something. On clouds that don't have a private address, use
|
||||
# the public.
|
||||
if not self._node.private_ipv4:
|
||||
self._node.private_ipv4 = server.public_v4
|
||||
|
||||
# Checkpoint save the updated node info
|
||||
self._zk.storeNode(self._node)
|
||||
|
||||
self.log.debug(
|
||||
"Node %s is running [region: %s, az: %s, ip: %s ipv4: %s, "
|
||||
"ipv6: %s]" %
|
||||
(self._node.id, self._node.region, self._node.az,
|
||||
self._node.interface_ip, self._node.public_ipv4,
|
||||
self._node.public_ipv6))
|
||||
|
||||
# Get the SSH public keys for the new node and record in ZooKeeper
|
||||
try:
|
||||
self.log.debug("Gathering host keys for node %s", self._node.id)
|
||||
host_keys = utils.keyscan(
|
||||
interface_ip, timeout=self._provider_config.boot_timeout)
|
||||
if not host_keys:
|
||||
raise exceptions.LaunchKeyscanException(
|
||||
"Unable to gather host keys")
|
||||
except exceptions.SSHTimeoutException:
|
||||
self.logConsole(self._node.external_id, self._node.hostname)
|
||||
raise
|
||||
|
||||
self._node.host_keys = host_keys
|
||||
self._zk.storeNode(self._node)
|
||||
|
||||
def _run(self):
|
||||
attempts = 1
|
||||
while attempts <= self._retries:
|
||||
try:
|
||||
self._launchNode()
|
||||
break
|
||||
except Exception as e:
|
||||
if attempts <= self._retries:
|
||||
self.log.exception(
|
||||
"Launch attempt %d/%d failed for node %s:",
|
||||
attempts, self._retries, self._node.id)
|
||||
# If we created an instance, delete it.
|
||||
if self._node.external_id:
|
||||
self._provider_manager.cleanupNode(self._node.external_id)
|
||||
self._provider_manager.waitForNodeCleanup(
|
||||
self._node.external_id
|
||||
)
|
||||
self._node.external_id = None
|
||||
self._node.public_ipv4 = None
|
||||
self._node.public_ipv6 = None
|
||||
self._node.interface_ip = None
|
||||
self._zk.storeNode(self._node)
|
||||
if attempts == self._retries:
|
||||
raise
|
||||
# Invalidate the quota cache if we encountered a quota error.
|
||||
if 'quota exceeded' in str(e).lower():
|
||||
self.log.info("Quota exceeded, invalidating quota cache")
|
||||
self._provider_manager.invalidateQuotaCache()
|
||||
attempts += 1
|
||||
|
||||
self._node.state = zk.READY
|
||||
self._zk.storeNode(self._node)
|
||||
self.log.info("Node id %s is ready", self._node.id)
|
||||
|
||||
def run(self):
|
||||
start_time = time.time()
|
||||
statsd_key = 'ready'
|
||||
|
||||
try:
|
||||
self._run()
|
||||
except Exception as e:
|
||||
self.log.exception("Launch failed for node %s:",
|
||||
self._node.id)
|
||||
self._node.state = zk.FAILED
|
||||
self._zk.storeNode(self._node)
|
||||
|
||||
if hasattr(e, 'statsd_key'):
|
||||
statsd_key = e.statsd_key
|
||||
else:
|
||||
statsd_key = 'error.unknown'
|
||||
|
||||
try:
|
||||
dt = int((time.time() - start_time) * 1000)
|
||||
self.recordLaunchStats(statsd_key, dt, self._image_name,
|
||||
self._node.provider, self._node.az,
|
||||
self._requestor)
|
||||
self.updateNodeStats(self._zk, self._provider_config)
|
||||
except Exception:
|
||||
self.log.exception("Exception while reporting stats:")
|
||||
|
||||
|
||||
class OpenStackNodeLaunchManager(NodeLaunchManager):
|
||||
def launch(self, node):
|
||||
'''
|
||||
Launch a new node as described by the supplied Node.
|
||||
|
||||
We expect each NodeLauncher thread to directly modify the node that
|
||||
is passed to it. The poll() method will expect to see the node.state
|
||||
attribute to change as the node is processed.
|
||||
|
||||
:param Node node: The node object.
|
||||
'''
|
||||
self._nodes.append(node)
|
||||
provider_label = self._pool.labels[node.type]
|
||||
t = NodeLauncher(self._zk, provider_label, self._provider_manager,
|
||||
self._requestor, node, self._retries)
|
||||
t.start()
|
||||
self._threads.append(t)
|
||||
|
||||
|
||||
class OpenStackNodeRequestHandler(NodeRequestHandler):
|
||||
|
||||
def __init__(self, pw, request):
|
||||
super(OpenStackNodeRequestHandler, self).__init__(pw, request)
|
||||
self.chosen_az = None
|
||||
self.log = logging.getLogger(
|
||||
"nodepool.driver.openstack.OpenStackNodeRequestHandler[%s]" %
|
||||
self.launcher_id)
|
||||
|
||||
def _imagesAvailable(self):
|
||||
'''
|
||||
Determines if the requested images are available for this provider.
|
||||
|
||||
ZooKeeper is queried for an image uploaded to the provider that is
|
||||
in the READY state.
|
||||
|
||||
:returns: True if it is available, False otherwise.
|
||||
'''
|
||||
for label in self.request.node_types:
|
||||
|
||||
if self.pool.labels[label].cloud_image:
|
||||
if not self.manager.labelReady(self.pool.labels[label]):
|
||||
return False
|
||||
else:
|
||||
if not self.zk.getMostRecentImageUpload(
|
||||
self.pool.labels[label].diskimage.name,
|
||||
self.provider.name):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _invalidNodeTypes(self):
|
||||
'''
|
||||
Return any node types that are invalid for this provider.
|
||||
|
||||
:returns: A list of node type names that are invalid, or an empty
|
||||
list if all are valid.
|
||||
'''
|
||||
invalid = []
|
||||
for ntype in self.request.node_types:
|
||||
if ntype not in self.pool.labels:
|
||||
invalid.append(ntype)
|
||||
return invalid
|
||||
|
||||
def _hasRemainingQuota(self, ntype):
|
||||
"""
|
||||
Checks if the predicted quota is enough for an additional node of type
|
||||
ntype.
|
||||
|
||||
:param ntype: node type for the quota check
|
||||
:return: True if there is enough quota, False otherwise
|
||||
"""
|
||||
|
||||
needed_quota = self.manager.quotaNeededByNodeType(ntype, self.pool)
|
||||
|
||||
# Calculate remaining quota which is calculated as:
|
||||
# quota = <total nodepool quota> - <used quota> - <quota for node>
|
||||
cloud_quota = self.manager.estimatedNodepoolQuota()
|
||||
cloud_quota.subtract(self.manager.estimatedNodepoolQuotaUsed(self.zk))
|
||||
cloud_quota.subtract(needed_quota)
|
||||
self.log.debug("Predicted remaining tenant quota: %s", cloud_quota)
|
||||
|
||||
if not cloud_quota.non_negative():
|
||||
return False
|
||||
|
||||
# Now calculate pool specific quota. Values indicating no quota default
|
||||
# to math.inf representing infinity that can be calculated with.
|
||||
pool_quota = QuotaInformation(cores=self.pool.max_cores,
|
||||
instances=self.pool.max_servers,
|
||||
ram=self.pool.max_ram,
|
||||
default=math.inf)
|
||||
pool_quota.subtract(
|
||||
self.manager.estimatedNodepoolQuotaUsed(self.zk, self.pool))
|
||||
pool_quota.subtract(needed_quota)
|
||||
self.log.debug("Predicted remaining pool quota: %s", pool_quota)
|
||||
|
||||
return pool_quota.non_negative()
|
||||
|
||||
def _hasProviderQuota(self, node_types):
|
||||
"""
|
||||
Checks if a provider has enough quota to handle a list of nodes.
|
||||
This does not take our currently existing nodes into account.
|
||||
|
||||
:param node_types: list of node types to check
|
||||
:return: True if the node list fits into the provider, False otherwise
|
||||
"""
|
||||
needed_quota = QuotaInformation()
|
||||
|
||||
for ntype in node_types:
|
||||
needed_quota.add(
|
||||
self.manager.quotaNeededByNodeType(ntype, self.pool))
|
||||
|
||||
cloud_quota = self.manager.estimatedNodepoolQuota()
|
||||
cloud_quota.subtract(needed_quota)
|
||||
|
||||
if not cloud_quota.non_negative():
|
||||
return False
|
||||
|
||||
# Now calculate pool specific quota. Values indicating no quota default
|
||||
# to math.inf representing infinity that can be calculated with.
|
||||
pool_quota = QuotaInformation(cores=self.pool.max_cores,
|
||||
instances=self.pool.max_servers,
|
||||
ram=self.pool.max_ram,
|
||||
default=math.inf)
|
||||
pool_quota.subtract(needed_quota)
|
||||
return pool_quota.non_negative()
|
||||
|
||||
def _waitForNodeSet(self):
|
||||
'''
|
||||
Fill node set for the request.
|
||||
|
||||
Obtain nodes for the request, pausing all new request handling for
|
||||
this provider until the node set can be filled.
|
||||
|
||||
We attempt to group the node set within the same provider availability
|
||||
zone. For this to work properly, the provider entry in the nodepool
|
||||
config must list the availability zones. Otherwise, new nodes will be
|
||||
put in random AZs at nova's whim. The exception being if there is an
|
||||
existing node in the READY state that we can select for this node set.
|
||||
Its AZ will then be used for new nodes, as well as any other READY
|
||||
nodes.
|
||||
|
||||
note:: This code is a bit racey in its calculation of the number of
|
||||
nodes in use for quota purposes. It is possible for multiple
|
||||
launchers to be doing this calculation at the same time. Since we
|
||||
currently have no locking mechanism around the "in use"
|
||||
calculation, if we are at the edge of the quota, one of the
|
||||
launchers could attempt to launch a new node after the other
|
||||
launcher has already started doing so. This would cause an
|
||||
expected failure from the underlying library, which is ok for now.
|
||||
'''
|
||||
if not self.launch_manager:
|
||||
self.launch_manager = OpenStackNodeLaunchManager(
|
||||
self.zk, self.pool, self.manager,
|
||||
self.request.requestor, retries=self.provider.launch_retries)
|
||||
|
||||
# Since this code can be called more than once for the same request,
|
||||
# we need to calculate the difference between our current node set
|
||||
# and what was requested. We cannot use set operations here since a
|
||||
# node type can appear more than once in the requested types.
|
||||
saved_types = collections.Counter([n.type for n in self.nodeset])
|
||||
requested_types = collections.Counter(self.request.node_types)
|
||||
diff = requested_types - saved_types
|
||||
needed_types = list(diff.elements())
|
||||
|
||||
ready_nodes = self.zk.getReadyNodesOfTypes(needed_types)
|
||||
|
||||
for ntype in needed_types:
|
||||
# First try to grab from the list of already available nodes.
|
||||
got_a_node = False
|
||||
if self.request.reuse and ntype in ready_nodes:
|
||||
for node in ready_nodes[ntype]:
|
||||
# Only interested in nodes from this provider and
|
||||
# pool, and within the selected AZ.
|
||||
if node.provider != self.provider.name:
|
||||
continue
|
||||
if node.pool != self.pool.name:
|
||||
continue
|
||||
if self.chosen_az and node.az != self.chosen_az:
|
||||
continue
|
||||
|
||||
try:
|
||||
self.zk.lockNode(node, blocking=False)
|
||||
except exceptions.ZKLockException:
|
||||
# It's already locked so skip it.
|
||||
continue
|
||||
else:
|
||||
if self.paused:
|
||||
self.log.debug("Unpaused request %s", self.request)
|
||||
self.paused = False
|
||||
|
||||
self.log.debug(
|
||||
"Locked existing node %s for request %s",
|
||||
node.id, self.request.id)
|
||||
got_a_node = True
|
||||
node.allocated_to = self.request.id
|
||||
self.zk.storeNode(node)
|
||||
self.nodeset.append(node)
|
||||
|
||||
# If we haven't already chosen an AZ, select the
|
||||
# AZ from this ready node. This will cause new nodes
|
||||
# to share this AZ, as well.
|
||||
if not self.chosen_az and node.az:
|
||||
self.chosen_az = node.az
|
||||
break
|
||||
|
||||
# Could not grab an existing node, so launch a new one.
|
||||
if not got_a_node:
|
||||
# Select grouping AZ if we didn't set AZ from a selected,
|
||||
# pre-existing node
|
||||
if not self.chosen_az:
|
||||
self.chosen_az = random.choice(
|
||||
self.pool.azs or self.manager.getAZs())
|
||||
|
||||
# If we calculate that we're at capacity, pause until nodes
|
||||
# are released by Zuul and removed by the DeletedNodeWorker.
|
||||
if not self._hasRemainingQuota(ntype):
|
||||
if not self.paused:
|
||||
self.log.debug(
|
||||
"Pausing request handling to satisfy request %s",
|
||||
self.request)
|
||||
self.paused = True
|
||||
return
|
||||
|
||||
if self.paused:
|
||||
self.log.debug("Unpaused request %s", self.request)
|
||||
self.paused = False
|
||||
|
||||
node = zk.Node()
|
||||
node.state = zk.INIT
|
||||
node.type = ntype
|
||||
node.provider = self.provider.name
|
||||
node.pool = self.pool.name
|
||||
node.az = self.chosen_az
|
||||
node.cloud = self.provider.cloud_config.name
|
||||
node.region = self.provider.region_name
|
||||
node.launcher = self.launcher_id
|
||||
node.allocated_to = self.request.id
|
||||
|
||||
# Note: It should be safe (i.e., no race) to lock the node
|
||||
# *after* it is stored since nodes in INIT state are not
|
||||
# locked anywhere.
|
||||
self.zk.storeNode(node)
|
||||
self.zk.lockNode(node, blocking=False)
|
||||
self.log.debug("Locked building node %s for request %s",
|
||||
node.id, self.request.id)
|
||||
|
||||
# Set state AFTER lock so that it isn't accidentally cleaned
|
||||
# up (unlocked BUILDING nodes will be deleted).
|
||||
node.state = zk.BUILDING
|
||||
self.zk.storeNode(node)
|
||||
|
||||
self.nodeset.append(node)
|
||||
self.launch_manager.launch(node)
|
||||
|
||||
def run_handler(self):
|
||||
'''
|
||||
Main body for the OpenStackNodeRequestHandler.
|
||||
'''
|
||||
self._setFromPoolWorker()
|
||||
|
||||
if self.provider is None or self.pool is None:
|
||||
# If the config changed out from underneath us, we could now be
|
||||
# an invalid provider and should stop handling this request.
|
||||
raise Exception("Provider configuration missing")
|
||||
|
||||
declined_reasons = []
|
||||
invalid_types = self._invalidNodeTypes()
|
||||
if invalid_types:
|
||||
declined_reasons.append('node type(s) [%s] not available' %
|
||||
','.join(invalid_types))
|
||||
elif not self._imagesAvailable():
|
||||
declined_reasons.append('images are not available')
|
||||
elif (self.pool.max_servers == 0 or
|
||||
not self._hasProviderQuota(self.request.node_types)):
|
||||
declined_reasons.append('it would exceed quota')
|
||||
# TODO(tobiash): Maybe also calculate the quota prediction here and
|
||||
# backoff for some seconds if the used quota would be exceeded?
|
||||
# This way we could give another (free) provider the chance to take
|
||||
# this request earlier.
|
||||
|
||||
# For min-ready requests, which do not re-use READY nodes, let's
|
||||
# decline if this provider is already at capacity. Otherwise, we
|
||||
# could end up wedged until another request frees up a node.
|
||||
if self.request.requestor == "NodePool:min-ready":
|
||||
current_count = self.zk.countPoolNodes(self.provider.name,
|
||||
self.pool.name)
|
||||
# Use >= because dynamic config changes to max-servers can leave
|
||||
# us with more than max-servers.
|
||||
if current_count >= self.pool.max_servers:
|
||||
declined_reasons.append("provider cannot satisify min-ready")
|
||||
|
||||
if declined_reasons:
|
||||
self.log.debug("Declining node request %s because %s",
|
||||
self.request.id, ', '.join(declined_reasons))
|
||||
self.decline_request()
|
||||
self.unlockNodeSet(clear_allocation=True)
|
||||
|
||||
# If conditions have changed for a paused request to now cause us
|
||||
# to decline it, we need to unpause so we don't keep trying it
|
||||
if self.paused:
|
||||
self.paused = False
|
||||
|
||||
self.zk.storeNodeRequest(self.request)
|
||||
self.zk.unlockNodeRequest(self.request)
|
||||
self.done = True
|
||||
return
|
||||
|
||||
if self.paused:
|
||||
self.log.debug("Retrying node request %s", self.request.id)
|
||||
else:
|
||||
self.log.debug("Accepting node request %s", self.request.id)
|
||||
self.request.state = zk.PENDING
|
||||
self.zk.storeNodeRequest(self.request)
|
||||
|
||||
self._waitForNodeSet()
|
540
nodepool/driver/openstack/provider.py
Executable file
540
nodepool/driver/openstack/provider.py
Executable file
@ -0,0 +1,540 @@
|
||||
# Copyright (C) 2011-2013 OpenStack Foundation
|
||||
#
|
||||
# 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 copy
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
import math
|
||||
import operator
|
||||
import time
|
||||
|
||||
import shade
|
||||
|
||||
from nodepool import exceptions
|
||||
from nodepool.driver import Provider
|
||||
from nodepool.nodeutils import iterate_timeout
|
||||
from nodepool.task_manager import ManagerStoppedException
|
||||
from nodepool.task_manager import TaskManager
|
||||
|
||||
|
||||
IPS_LIST_AGE = 5 # How long to keep a cached copy of the ip list
|
||||
MAX_QUOTA_AGE = 5 * 60 # How long to keep the quota information cached
|
||||
|
||||
|
||||
@contextmanager
|
||||
def shade_inner_exceptions():
|
||||
try:
|
||||
yield
|
||||
except shade.OpenStackCloudException as e:
|
||||
e.log_error()
|
||||
raise
|
||||
|
||||
|
||||
class QuotaInformation:
|
||||
|
||||
def __init__(self, cores=None, instances=None, ram=None, default=0):
|
||||
'''
|
||||
Initializes the quota information with some values. None values will
|
||||
be initialized with default which will be typically 0 or math.inf
|
||||
indicating an infinite limit.
|
||||
|
||||
:param cores:
|
||||
:param instances:
|
||||
:param ram:
|
||||
:param default:
|
||||
'''
|
||||
self.quota = {
|
||||
'compute': {
|
||||
'cores': self._get_default(cores, default),
|
||||
'instances': self._get_default(instances, default),
|
||||
'ram': self._get_default(ram, default),
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def construct_from_flavor(flavor):
|
||||
return QuotaInformation(instances=1,
|
||||
cores=flavor.vcpus,
|
||||
ram=flavor.ram)
|
||||
|
||||
@staticmethod
|
||||
def construct_from_limits(limits):
|
||||
def bound_value(value):
|
||||
if value == -1:
|
||||
return math.inf
|
||||
return value
|
||||
|
||||
return QuotaInformation(
|
||||
instances=bound_value(limits.max_total_instances),
|
||||
cores=bound_value(limits.max_total_cores),
|
||||
ram=bound_value(limits.max_total_ram_size))
|
||||
|
||||
def _get_default(self, value, default):
|
||||
return value if value is not None else default
|
||||
|
||||
def _add_subtract(self, other, add=True):
|
||||
for category in self.quota.keys():
|
||||
for resource in self.quota[category].keys():
|
||||
second_value = other.quota.get(category, {}).get(resource, 0)
|
||||
if add:
|
||||
self.quota[category][resource] += second_value
|
||||
else:
|
||||
self.quota[category][resource] -= second_value
|
||||
|
||||
def subtract(self, other):
|
||||
self._add_subtract(other, add=False)
|
||||
|
||||
def add(self, other):
|
||||
self._add_subtract(other, True)
|
||||
|
||||
def non_negative(self):
|
||||
for key_i, category in self.quota.items():
|
||||
for resource, value in category.items():
|
||||
if value < 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return str(self.quota)
|
||||
|
||||
|
||||
class OpenStackProvider(Provider):
|
||||
log = logging.getLogger("nodepool.driver.openstack.OpenStackProvider")
|
||||
|
||||
def __init__(self, provider, use_taskmanager):
|
||||
self.provider = provider
|
||||
self._images = {}
|
||||
self._networks = {}
|
||||
self.__flavors = {}
|
||||
self.__azs = None
|
||||
self._use_taskmanager = use_taskmanager
|
||||
self._taskmanager = None
|
||||
self._current_nodepool_quota = None
|
||||
|
||||
def start(self):
|
||||
if self._use_taskmanager:
|
||||
self._taskmanager = TaskManager(None, self.provider.name,
|
||||
self.provider.rate)
|
||||
self._taskmanager.start()
|
||||
self.resetClient()
|
||||
|
||||
def stop(self):
|
||||
if self._taskmanager:
|
||||
self._taskmanager.stop()
|
||||
|
||||
def join(self):
|
||||
if self._taskmanager:
|
||||
self._taskmanager.join()
|
||||
|
||||
@property
|
||||
def _flavors(self):
|
||||
if not self.__flavors:
|
||||
self.__flavors = self._getFlavors()
|
||||
return self.__flavors
|
||||
|
||||
def _getClient(self):
|
||||
if self._use_taskmanager:
|
||||
manager = self._taskmanager
|
||||
else:
|
||||
manager = None
|
||||
return shade.OpenStackCloud(
|
||||
cloud_config=self.provider.cloud_config,
|
||||
manager=manager,
|
||||
**self.provider.cloud_config.config)
|
||||
|
||||
def quotaNeededByNodeType(self, ntype, pool):
|
||||
provider_label = pool.labels[ntype]
|
||||
|
||||
flavor = self.findFlavor(provider_label.flavor_name,
|
||||
provider_label.min_ram)
|
||||
|
||||
return QuotaInformation.construct_from_flavor(flavor)
|
||||
|
||||
def estimatedNodepoolQuota(self):
|
||||
'''
|
||||
Determine how much quota is available for nodepool managed resources.
|
||||
This needs to take into account the quota of the tenant, resources
|
||||
used outside of nodepool and the currently used resources by nodepool,
|
||||
max settings in nodepool config. This is cached for MAX_QUOTA_AGE
|
||||
seconds.
|
||||
|
||||
:return: Total amount of resources available which is currently
|
||||
available to nodepool including currently existing nodes.
|
||||
'''
|
||||
|
||||
if self._current_nodepool_quota:
|
||||
now = time.time()
|
||||
if now < self._current_nodepool_quota['timestamp'] + MAX_QUOTA_AGE:
|
||||
return copy.deepcopy(self._current_nodepool_quota['quota'])
|
||||
|
||||
with shade_inner_exceptions():
|
||||
limits = self._client.get_compute_limits()
|
||||
|
||||
# This is initialized with the full tenant quota and later becomes
|
||||
# the quota available for nodepool.
|
||||
nodepool_quota = QuotaInformation.construct_from_limits(limits)
|
||||
self.log.debug("Provider quota for %s: %s",
|
||||
self.provider.name, nodepool_quota)
|
||||
|
||||
# Subtract the unmanaged quota usage from nodepool_max
|
||||
# to get the quota available for us.
|
||||
nodepool_quota.subtract(self.unmanagedQuotaUsed())
|
||||
|
||||
self._current_nodepool_quota = {
|
||||
'quota': nodepool_quota,
|
||||
'timestamp': time.time()
|
||||
}
|
||||
|
||||
self.log.debug("Available quota for %s: %s",
|
||||
self.provider.name, nodepool_quota)
|
||||
|
||||
return copy.deepcopy(nodepool_quota)
|
||||
|
||||
def invalidateQuotaCache(self):
|
||||
self._current_nodepool_quota['timestamp'] = 0
|
||||
|
||||
def estimatedNodepoolQuotaUsed(self, zk, pool=None):
|
||||
'''
|
||||
Sums up the quota used (or planned) currently by nodepool. If pool is
|
||||
given it is filtered by the pool.
|
||||
|
||||
:param zk: the object to access zookeeper
|
||||
:param pool: If given, filtered by the pool.
|
||||
:return: Calculated quota in use by nodepool
|
||||
'''
|
||||
used_quota = QuotaInformation()
|
||||
|
||||
for node in zk.nodeIterator():
|
||||
if node.provider == self.provider.name:
|
||||
if pool and not node.pool == pool.name:
|
||||
continue
|
||||
provider_pool = self.provider.pools.get(node.pool)
|
||||
if not provider_pool:
|
||||
self.log.warning(
|
||||
"Cannot find provider pool for node %s" % node)
|
||||
# This node is in a funny state we log it for debugging
|
||||
# but move on and don't account it as we can't properly
|
||||
# calculate its cost without pool info.
|
||||
continue
|
||||
node_resources = self.quotaNeededByNodeType(
|
||||
node.type, provider_pool)
|
||||
used_quota.add(node_resources)
|
||||
return used_quota
|
||||
|
||||
def unmanagedQuotaUsed(self):
|
||||
'''
|
||||
Sums up the quota used by servers unmanaged by nodepool.
|
||||
|
||||
:return: Calculated quota in use by unmanaged servers
|
||||
'''
|
||||
flavors = self.listFlavorsById()
|
||||
used_quota = QuotaInformation()
|
||||
|
||||
for server in self.listNodes():
|
||||
meta = server.get('metadata', {})
|
||||
|
||||
nodepool_provider_name = meta.get('nodepool_provider_name')
|
||||
if nodepool_provider_name and \
|
||||
nodepool_provider_name == self.provider.name:
|
||||
# This provider (regardless of the launcher) owns this server
|
||||
# so it must not be accounted for unmanaged quota.
|
||||
continue
|
||||
|
||||
flavor = flavors.get(server.flavor.id)
|
||||
used_quota.add(QuotaInformation.construct_from_flavor(flavor))
|
||||
|
||||
return used_quota
|
||||
|
||||
def resetClient(self):
|
||||
self._client = self._getClient()
|
||||
if self._use_taskmanager:
|
||||
self._taskmanager.setClient(self._client)
|
||||
|
||||
def _getFlavors(self):
|
||||
flavors = self.listFlavors()
|
||||
flavors.sort(key=operator.itemgetter('ram'))
|
||||
return flavors
|
||||
|
||||
# TODO(mordred): These next three methods duplicate logic that is in
|
||||
# shade, but we can't defer to shade until we're happy
|
||||
# with using shade's resource caching facility. We have
|
||||
# not yet proven that to our satisfaction, but if/when
|
||||
# we do, these should be able to go away.
|
||||
def _findFlavorByName(self, flavor_name):
|
||||
for f in self._flavors:
|
||||
if flavor_name in (f['name'], f['id']):
|
||||
return f
|
||||
raise Exception("Unable to find flavor: %s" % flavor_name)
|
||||
|
||||
def _findFlavorByRam(self, min_ram, flavor_name):
|
||||
for f in self._flavors:
|
||||
if (f['ram'] >= min_ram
|
||||
and (not flavor_name or flavor_name in f['name'])):
|
||||
return f
|
||||
raise Exception("Unable to find flavor with min ram: %s" % min_ram)
|
||||
|
||||
def findFlavor(self, flavor_name, min_ram):
|
||||
# Note: this will throw an error if the provider is offline
|
||||
# but all the callers are in threads (they call in via CreateServer) so
|
||||
# the mainloop won't be affected.
|
||||
if min_ram:
|
||||
return self._findFlavorByRam(min_ram, flavor_name)
|
||||
else:
|
||||
return self._findFlavorByName(flavor_name)
|
||||
|
||||
def findImage(self, name):
|
||||
if name in self._images:
|
||||
return self._images[name]
|
||||
|
||||
with shade_inner_exceptions():
|
||||
image = self._client.get_image(name)
|
||||
self._images[name] = image
|
||||
return image
|
||||
|
||||
def findNetwork(self, name):
|
||||
if name in self._networks:
|
||||
return self._networks[name]
|
||||
|
||||
with shade_inner_exceptions():
|
||||
network = self._client.get_network(name)
|
||||
self._networks[name] = network
|
||||
return network
|
||||
|
||||
def deleteImage(self, name):
|
||||
if name in self._images:
|
||||
del self._images[name]
|
||||
|
||||
with shade_inner_exceptions():
|
||||
return self._client.delete_image(name)
|
||||
|
||||
def createServer(self, name, image,
|
||||
flavor_name=None, min_ram=None,
|
||||
az=None, key_name=None, config_drive=True,
|
||||
nodepool_node_id=None, nodepool_node_label=None,
|
||||
nodepool_image_name=None,
|
||||
networks=None, boot_from_volume=False, volume_size=50):
|
||||
if not networks:
|
||||
networks = []
|
||||
if not isinstance(image, dict):
|
||||
# if it's a dict, we already have the cloud id. If it's not,
|
||||
# we don't know if it's name or ID so need to look it up
|
||||
image = self.findImage(image)
|
||||
flavor = self.findFlavor(flavor_name=flavor_name, min_ram=min_ram)
|
||||
create_args = dict(name=name,
|
||||
image=image,
|
||||
flavor=flavor,
|
||||
config_drive=config_drive)
|
||||
if boot_from_volume:
|
||||
create_args['boot_from_volume'] = boot_from_volume
|
||||
create_args['volume_size'] = volume_size
|
||||
# NOTE(pabelanger): Always cleanup volumes when we delete a server.
|
||||
create_args['terminate_volume'] = True
|
||||
if key_name:
|
||||
create_args['key_name'] = key_name
|
||||
if az:
|
||||
create_args['availability_zone'] = az
|
||||
nics = []
|
||||
for network in networks:
|
||||
net_id = self.findNetwork(network)['id']
|
||||
nics.append({'net-id': net_id})
|
||||
if nics:
|
||||
create_args['nics'] = nics
|
||||
# Put provider.name and image_name in as groups so that ansible
|
||||
# inventory can auto-create groups for us based on each of those
|
||||
# qualities
|
||||
# Also list each of those values directly so that non-ansible
|
||||
# consumption programs don't need to play a game of knowing that
|
||||
# groups[0] is the image name or anything silly like that.
|
||||
groups_list = [self.provider.name]
|
||||
|
||||
if nodepool_image_name:
|
||||
groups_list.append(nodepool_image_name)
|
||||
if nodepool_node_label:
|
||||
groups_list.append(nodepool_node_label)
|
||||
meta = dict(
|
||||
groups=",".join(groups_list),
|
||||
nodepool_provider_name=self.provider.name,
|
||||
)
|
||||
if nodepool_node_id:
|
||||
meta['nodepool_node_id'] = nodepool_node_id
|
||||
if nodepool_image_name:
|
||||
meta['nodepool_image_name'] = nodepool_image_name
|
||||
if nodepool_node_label:
|
||||
meta['nodepool_node_label'] = nodepool_node_label
|
||||
create_args['meta'] = meta
|
||||
|
||||
with shade_inner_exceptions():
|
||||
return self._client.create_server(wait=False, **create_args)
|
||||
|
||||
def getServer(self, server_id):
|
||||
with shade_inner_exceptions():
|
||||
return self._client.get_server(server_id)
|
||||
|
||||
def getServerConsole(self, server_id):
|
||||
try:
|
||||
with shade_inner_exceptions():
|
||||
return self._client.get_server_console(server_id)
|
||||
except shade.OpenStackCloudException:
|
||||
return None
|
||||
|
||||
def waitForServer(self, server, timeout=3600, auto_ip=True):
|
||||
with shade_inner_exceptions():
|
||||
return self._client.wait_for_server(
|
||||
server=server, auto_ip=auto_ip,
|
||||
reuse=False, timeout=timeout)
|
||||
|
||||
def waitForNodeCleanup(self, server_id, timeout=600):
|
||||
for count in iterate_timeout(
|
||||
timeout, exceptions.ServerDeleteException,
|
||||
"server %s deletion" % server_id):
|
||||
if not self.getServer(server_id):
|
||||
return
|
||||
|
||||
def waitForImage(self, image_id, timeout=3600):
|
||||
last_status = None
|
||||
for count in iterate_timeout(
|
||||
timeout, exceptions.ImageCreateException, "image creation"):
|
||||
try:
|
||||
image = self.getImage(image_id)
|
||||
except exceptions.NotFound:
|
||||
continue
|
||||
except ManagerStoppedException:
|
||||
raise
|
||||
except Exception:
|
||||
self.log.exception('Unable to list images while waiting for '
|
||||
'%s will retry' % (image_id))
|
||||
continue
|
||||
|
||||
# shade returns None when not found
|
||||
if not image:
|
||||
continue
|
||||
|
||||
status = image['status']
|
||||
if (last_status != status):
|
||||
self.log.debug(
|
||||
'Status of image in {provider} {id}: {status}'.format(
|
||||
provider=self.provider.name,
|
||||
id=image_id,
|
||||
status=status))
|
||||
if status == 'ERROR' and 'fault' in image:
|
||||
self.log.debug(
|
||||
'ERROR in {provider} on {id}: {resason}'.format(
|
||||
provider=self.provider.name,
|
||||
id=image_id,
|
||||
resason=image['fault']['message']))
|
||||
last_status = status
|
||||
# Glance client returns lower case statuses - but let's be sure
|
||||
if status.lower() in ['active', 'error']:
|
||||
return image
|
||||
|
||||
def createImage(self, server, image_name, meta):
|
||||
with shade_inner_exceptions():
|
||||
return self._client.create_image_snapshot(
|
||||
image_name, server, **meta)
|
||||
|
||||
def getImage(self, image_id):
|
||||
with shade_inner_exceptions():
|
||||
return self._client.get_image(image_id)
|
||||
|
||||
def labelReady(self, label):
|
||||
if not label.cloud_image:
|
||||
return False
|
||||
image = self.getImage(label.cloud_image.external)
|
||||
if not image:
|
||||
self.log.warning(
|
||||
"Provider %s is configured to use %s as the"
|
||||
" cloud-image for label %s and that"
|
||||
" cloud-image could not be found in the"
|
||||
" cloud." % (self.provider.name,
|
||||
label.cloud_image.external_name,
|
||||
label.name))
|
||||
return False
|
||||
return True
|
||||
|
||||
def uploadImage(self, image_name, filename, image_type=None, meta=None,
|
||||
md5=None, sha256=None):
|
||||
# configure glance and upload image. Note the meta flags
|
||||
# are provided as custom glance properties
|
||||
# NOTE: we have wait=True set here. This is not how we normally
|
||||
# do things in nodepool, preferring to poll ourselves thankyouverymuch.
|
||||
# However - two things to note:
|
||||
# - PUT has no aysnc mechanism, so we have to handle it anyway
|
||||
# - v2 w/task waiting is very strange and complex - but we have to
|
||||
# block for our v1 clouds anyway, so we might as well
|
||||
# have the interface be the same and treat faking-out
|
||||
# a shade-level fake-async interface later
|
||||
if not meta:
|
||||
meta = {}
|
||||
if image_type:
|
||||
meta['disk_format'] = image_type
|
||||
with shade_inner_exceptions():
|
||||
image = self._client.create_image(
|
||||
name=image_name,
|
||||
filename=filename,
|
||||
is_public=False,
|
||||
wait=True,
|
||||
md5=md5,
|
||||
sha256=sha256,
|
||||
**meta)
|
||||
return image.id
|
||||
|
||||
def listImages(self):
|
||||
with shade_inner_exceptions():
|
||||
return self._client.list_images()
|
||||
|
||||
def listFlavors(self):
|
||||
with shade_inner_exceptions():
|
||||
return self._client.list_flavors(get_extra=False)
|
||||
|
||||
def listFlavorsById(self):
|
||||
with shade_inner_exceptions():
|
||||
flavors = {}
|
||||
for flavor in self._client.list_flavors(get_extra=False):
|
||||
flavors[flavor.id] = flavor
|
||||
return flavors
|
||||
|
||||
def listNodes(self):
|
||||
# shade list_servers carries the nodepool server list caching logic
|
||||
with shade_inner_exceptions():
|
||||
return self._client.list_servers()
|
||||
|
||||
def deleteServer(self, server_id):
|
||||
with shade_inner_exceptions():
|
||||
return self._client.delete_server(server_id, delete_ips=True)
|
||||
|
||||
def cleanupNode(self, server_id):
|
||||
server = self.getServer(server_id)
|
||||
if not server:
|
||||
raise exceptions.NotFound()
|
||||
|
||||
self.log.debug('Deleting server %s' % server_id)
|
||||
self.deleteServer(server_id)
|
||||
|
||||
def cleanupLeakedResources(self):
|
||||
if self.provider.clean_floating_ips:
|
||||
with shade_inner_exceptions():
|
||||
self._client.delete_unattached_floating_ips()
|
||||
|
||||
def getAZs(self):
|
||||
if self.__azs is None:
|
||||
self.__azs = self._client.list_availability_zone_names()
|
||||
if not self.__azs:
|
||||
# If there are no zones, return a list containing None so that
|
||||
# random.choice can pick None and pass that to Nova. If this
|
||||
# feels dirty, please direct your ire to policy.json and the
|
||||
# ability to turn off random portions of the OpenStack API.
|
||||
self.__azs = [None]
|
||||
return self.__azs
|
22
nodepool/exceptions.py
Normal file → Executable file
22
nodepool/exceptions.py
Normal file → Executable file
@ -13,6 +13,26 @@
|
||||
# under the License.
|
||||
|
||||
|
||||
class NotFound(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LaunchNodepoolException(Exception):
|
||||
statsd_key = 'error.nodepool'
|
||||
|
||||
|
||||
class LaunchStatusException(Exception):
|
||||
statsd_key = 'error.status'
|
||||
|
||||
|
||||
class LaunchNetworkException(Exception):
|
||||
statsd_key = 'error.network'
|
||||
|
||||
|
||||
class LaunchKeyscanException(Exception):
|
||||
statsd_key = 'error.keyscan'
|
||||
|
||||
|
||||
class BuilderError(RuntimeError):
|
||||
pass
|
||||
|
||||
@ -44,8 +64,10 @@ class ServerDeleteException(TimeoutException):
|
||||
class ImageCreateException(TimeoutException):
|
||||
statsd_key = 'error.imagetimeout'
|
||||
|
||||
|
||||
class ZKException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ZKLockException(ZKException):
|
||||
pass
|
||||
|
@ -1,145 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright (C) 2011-2013 OpenStack Foundation
|
||||
#
|
||||
# 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 re
|
||||
|
||||
import jenkins
|
||||
import fakeprovider
|
||||
from task_manager import Task, TaskManager
|
||||
|
||||
|
||||
class CreateNodeTask(Task):
|
||||
def main(self, jenkins):
|
||||
if 'credentials_id' in self.args:
|
||||
launcher_params = {'port': 22,
|
||||
'credentialsId': self.args['credentials_id'],
|
||||
'sshHostKeyVerificationStrategy':
|
||||
{'stapler-class':
|
||||
('hudson.plugins.sshslaves.verifiers.'
|
||||
'NonVerifyingKeyVerificationStrategy')},
|
||||
'host': self.args['host']}
|
||||
else:
|
||||
launcher_params = {'port': 22,
|
||||
'username': self.args['username'],
|
||||
'privatekey': self.args['private_key'],
|
||||
'sshHostKeyVerificationStrategy':
|
||||
{'stapler-class':
|
||||
('hudson.plugins.sshslaves.verifiers.'
|
||||
'NonVerifyingKeyVerificationStrategy')},
|
||||
'host': self.args['host']}
|
||||
args = dict(
|
||||
name=self.args['name'],
|
||||
numExecutors=self.args['executors'],
|
||||
nodeDescription=self.args['description'],
|
||||
remoteFS=self.args['root'],
|
||||
exclusive=True,
|
||||
launcher='hudson.plugins.sshslaves.SSHLauncher',
|
||||
launcher_params=launcher_params)
|
||||
if self.args['labels']:
|
||||
args['labels'] = self.args['labels']
|
||||
try:
|
||||
jenkins.create_node(**args)
|
||||
except jenkins.JenkinsException as e:
|
||||
if 'already exists' in str(e):
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
class NodeExistsTask(Task):
|
||||
def main(self, jenkins):
|
||||
return jenkins.node_exists(self.args['name'])
|
||||
|
||||
|
||||
class DeleteNodeTask(Task):
|
||||
def main(self, jenkins):
|
||||
return jenkins.delete_node(self.args['name'])
|
||||
|
||||
|
||||
class GetNodeConfigTask(Task):
|
||||
def main(self, jenkins):
|
||||
return jenkins.get_node_config(self.args['name'])
|
||||
|
||||
|
||||
class SetNodeConfigTask(Task):
|
||||
def main(self, jenkins):
|
||||
jenkins.reconfig_node(self.args['name'], self.args['config'])
|
||||
|
||||
|
||||
class StartBuildTask(Task):
|
||||
def main(self, jenkins):
|
||||
jenkins.build_job(self.args['name'],
|
||||
parameters=self.args['params'])
|
||||
|
||||
|
||||
class GetInfoTask(Task):
|
||||
def main(self, jenkins):
|
||||
return jenkins.get_info()
|
||||
|
||||
|
||||
class JenkinsManager(TaskManager):
|
||||
log = logging.getLogger("nodepool.JenkinsManager")
|
||||
|
||||
def __init__(self, target):
|
||||
super(JenkinsManager, self).__init__(None, target.name, target.rate)
|
||||
self.target = target
|
||||
self._client = self._getClient()
|
||||
|
||||
def _getClient(self):
|
||||
if self.target.jenkins_apikey == 'fake':
|
||||
return fakeprovider.FakeJenkins(self.target.jenkins_user)
|
||||
return jenkins.Jenkins(self.target.jenkins_url,
|
||||
self.target.jenkins_user,
|
||||
self.target.jenkins_apikey)
|
||||
|
||||
def createNode(self, name, host, description, executors, root, labels=[],
|
||||
credentials_id=None, username=None, private_key=None):
|
||||
args = dict(name=name, host=host, description=description,
|
||||
labels=labels, executors=executors, root=root)
|
||||
if credentials_id:
|
||||
args['credentials_id'] = credentials_id
|
||||
else:
|
||||
args['username'] = username
|
||||
args['private_key'] = private_key
|
||||
return self.submitTask(CreateNodeTask(**args))
|
||||
|
||||
def nodeExists(self, name):
|
||||
return self.submitTask(NodeExistsTask(name=name))
|
||||
|
||||
def deleteNode(self, name):
|
||||
return self.submitTask(DeleteNodeTask(name=name))
|
||||
|
||||
LABEL_RE = re.compile(r'<label>(.*)</label>')
|
||||
|
||||
def relabelNode(self, name, labels):
|
||||
config = self.submitTask(GetNodeConfigTask(name=name))
|
||||
old = None
|
||||
m = self.LABEL_RE.search(config)
|
||||
if m:
|
||||
old = m.group(1)
|
||||
config = self.LABEL_RE.sub('<label>%s</label>' % ' '.join(labels),
|
||||
config)
|
||||
self.submitTask(SetNodeConfigTask(name=name, config=config))
|
||||
return old
|
||||
|
||||
def startBuild(self, name, params):
|
||||
self.submitTask(StartBuildTask(name=name, params=params))
|
||||
|
||||
def getInfo(self):
|
||||
return self._client.get_info()
|
@ -1,78 +0,0 @@
|
||||
# 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 json
|
||||
import logging
|
||||
import uuid
|
||||
import threading
|
||||
|
||||
import gear
|
||||
|
||||
|
||||
class WatchableJob(gear.Job):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(WatchableJob, self).__init__(*args, **kwargs)
|
||||
self._completion_handlers = []
|
||||
self._event = threading.Event()
|
||||
|
||||
def _handleCompletion(self, mode=None):
|
||||
self._event.set()
|
||||
for handler in self._completion_handlers:
|
||||
handler(self)
|
||||
|
||||
def addCompletionHandler(self, handler):
|
||||
self._completion_handlers.append(handler)
|
||||
|
||||
def onCompleted(self):
|
||||
self._handleCompletion()
|
||||
|
||||
def onFailed(self):
|
||||
self._handleCompletion()
|
||||
|
||||
def onDisconnect(self):
|
||||
self._handleCompletion()
|
||||
|
||||
def onWorkStatus(self):
|
||||
pass
|
||||
|
||||
def waitForCompletion(self, timeout=None):
|
||||
return self._event.wait(timeout)
|
||||
|
||||
|
||||
class NodepoolJob(WatchableJob):
|
||||
def __init__(self, job_name, job_data_obj, nodepool):
|
||||
job_uuid = str(uuid.uuid4().hex)
|
||||
job_data = json.dumps(job_data_obj)
|
||||
super(NodepoolJob, self).__init__(job_name, job_data, job_uuid)
|
||||
self.nodepool = nodepool
|
||||
|
||||
def getDbSession(self):
|
||||
return self.nodepool.getDB().getSession()
|
||||
|
||||
|
||||
class NodeAssignmentJob(NodepoolJob):
|
||||
log = logging.getLogger("jobs.NodeAssignmentJob")
|
||||
|
||||
def __init__(self, node_id, target_name, data, nodepool):
|
||||
self.node_id = node_id
|
||||
job_name = 'node_assign:%s' % target_name
|
||||
super(NodeAssignmentJob, self).__init__(job_name, data, nodepool)
|
||||
|
||||
|
||||
class NodeRevokeJob(NodepoolJob):
|
||||
log = logging.getLogger("jobs.NodeRevokeJob")
|
||||
|
||||
def __init__(self, node_id, manager_name, data, nodepool):
|
||||
self.node_id = node_id
|
||||
job_name = 'node_revoke:%s' % manager_name
|
||||
super(NodeRevokeJob, self).__init__(job_name, data, nodepool)
|
955
nodepool/launcher.py
Executable file
955
nodepool/launcher.py
Executable file
@ -0,0 +1,955 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright (C) 2011-2014 OpenStack Foundation
|
||||
#
|
||||
# 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 os.path
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
from nodepool import exceptions
|
||||
from nodepool import provider_manager
|
||||
from nodepool import stats
|
||||
from nodepool import config as nodepool_config
|
||||
from nodepool import zk
|
||||
from nodepool.driver.fake.handler import FakeNodeRequestHandler
|
||||
from nodepool.driver.openstack.handler import OpenStackNodeRequestHandler
|
||||
|
||||
|
||||
MINS = 60
|
||||
HOURS = 60 * MINS
|
||||
|
||||
# Interval between checking if new servers needed
|
||||
WATERMARK_SLEEP = 10
|
||||
|
||||
# When to delete node request lock znodes
|
||||
LOCK_CLEANUP = 8 * HOURS
|
||||
|
||||
# How long to wait between checks for ZooKeeper connectivity if it disappears.
|
||||
SUSPEND_WAIT_TIME = 30
|
||||
|
||||
|
||||
class NodeDeleter(threading.Thread, stats.StatsReporter):
|
||||
log = logging.getLogger("nodepool.NodeDeleter")
|
||||
|
||||
def __init__(self, zk, provider_manager, node):
|
||||
threading.Thread.__init__(self, name='NodeDeleter for %s %s' %
|
||||
(node.provider, node.external_id))
|
||||
stats.StatsReporter.__init__(self)
|
||||
self._zk = zk
|
||||
self._provider_manager = provider_manager
|
||||
self._node = node
|
||||
|
||||
@staticmethod
|
||||
def delete(zk_conn, manager, node, node_exists=True):
|
||||
'''
|
||||
Delete a server instance and ZooKeeper node.
|
||||
|
||||
This is a class method so we can support instantaneous deletes.
|
||||
|
||||
:param ZooKeeper zk_conn: A ZooKeeper object to use.
|
||||
:param ProviderManager provider_manager: ProviderManager object to
|
||||
use fo deleting the server.
|
||||
:param Node node: A locked Node object that describes the server to
|
||||
delete.
|
||||
:param bool node_exists: True if the node actually exists in ZooKeeper.
|
||||
An artifical Node object can be passed that can be used to delete
|
||||
a leaked instance.
|
||||
'''
|
||||
try:
|
||||
node.state = zk.DELETING
|
||||
zk_conn.storeNode(node)
|
||||
if node.external_id:
|
||||
manager.cleanupNode(node.external_id)
|
||||
manager.waitForNodeCleanup(node.external_id)
|
||||
except exceptions.NotFound:
|
||||
NodeDeleter.log.info("Instance %s not found in provider %s",
|
||||
node.external_id, node.provider)
|
||||
except Exception:
|
||||
NodeDeleter.log.exception(
|
||||
"Exception deleting instance %s from %s:",
|
||||
node.external_id, node.provider)
|
||||
# Don't delete the ZK node in this case, but do unlock it
|
||||
if node_exists:
|
||||
zk_conn.unlockNode(node)
|
||||
return
|
||||
|
||||
if node_exists:
|
||||
NodeDeleter.log.info(
|
||||
"Deleting ZK node id=%s, state=%s, external_id=%s",
|
||||
node.id, node.state, node.external_id)
|
||||
# This also effectively releases the lock
|
||||
zk_conn.deleteNode(node)
|
||||
|
||||
def run(self):
|
||||
# Since leaked instances won't have an actual node in ZooKeeper,
|
||||
# we need to check 'id' to see if this is an artificial Node.
|
||||
if self._node.id is None:
|
||||
node_exists = False
|
||||
else:
|
||||
node_exists = True
|
||||
|
||||
self.delete(self._zk, self._provider_manager, self._node, node_exists)
|
||||
|
||||
try:
|
||||
self.updateNodeStats(self._zk, self._provider_manager.provider)
|
||||
except Exception:
|
||||
self.log.exception("Exception while reporting stats:")
|
||||
|
||||
|
||||
class PoolWorker(threading.Thread):
|
||||
'''
|
||||
Class that manages node requests for a single provider pool.
|
||||
|
||||
The NodePool thread will instantiate a class of this type for each
|
||||
provider pool found in the nodepool configuration file. If the
|
||||
pool or provider to which this thread is assigned is removed from
|
||||
the configuration file, then that will be recognized and this
|
||||
thread will shut itself down.
|
||||
'''
|
||||
|
||||
def __init__(self, nodepool, provider_name, pool_name):
|
||||
threading.Thread.__init__(
|
||||
self, name='PoolWorker.%s-%s' % (provider_name, pool_name)
|
||||
)
|
||||
self.log = logging.getLogger("nodepool.%s" % self.name)
|
||||
self.nodepool = nodepool
|
||||
self.provider_name = provider_name
|
||||
self.pool_name = pool_name
|
||||
self.running = False
|
||||
self.paused_handler = None
|
||||
self.request_handlers = []
|
||||
self.watermark_sleep = nodepool.watermark_sleep
|
||||
self.zk = self.getZK()
|
||||
self.launcher_id = "%s-%s-%s" % (socket.gethostname(),
|
||||
os.getpid(),
|
||||
self.name)
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# Private methods
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
def _get_node_request_handler(self, provider, request):
|
||||
if provider.driver.name == 'fake':
|
||||
return FakeNodeRequestHandler(self, request)
|
||||
elif provider.driver.name == 'openstack':
|
||||
return OpenStackNodeRequestHandler(self, request)
|
||||
else:
|
||||
raise RuntimeError("Unknown provider driver %s" % provider.driver)
|
||||
|
||||
def _assignHandlers(self):
|
||||
'''
|
||||
For each request we can grab, create a NodeRequestHandler for it.
|
||||
|
||||
The NodeRequestHandler object will kick off any threads needed to
|
||||
satisfy the request, then return. We will need to periodically poll
|
||||
the handler for completion.
|
||||
'''
|
||||
provider = self.getProviderConfig()
|
||||
if not provider:
|
||||
self.log.info("Missing config. Deleted provider?")
|
||||
return
|
||||
|
||||
if provider.max_concurrency == 0:
|
||||
return
|
||||
|
||||
for req_id in self.zk.getNodeRequests():
|
||||
if self.paused_handler:
|
||||
return
|
||||
|
||||
# Get active threads for all pools for this provider
|
||||
active_threads = sum([
|
||||
w.activeThreads() for
|
||||
w in self.nodepool.getPoolWorkers(self.provider_name)
|
||||
])
|
||||
|
||||
# Short-circuit for limited request handling
|
||||
if (provider.max_concurrency > 0 and
|
||||
active_threads >= provider.max_concurrency):
|
||||
self.log.debug("Request handling limited: %s active threads ",
|
||||
"with max concurrency of %s",
|
||||
active_threads, provider.max_concurrency)
|
||||
return
|
||||
|
||||
req = self.zk.getNodeRequest(req_id)
|
||||
if not req:
|
||||
continue
|
||||
|
||||
# Only interested in unhandled requests
|
||||
if req.state != zk.REQUESTED:
|
||||
continue
|
||||
|
||||
# Skip it if we've already declined
|
||||
if self.launcher_id in req.declined_by:
|
||||
continue
|
||||
|
||||
try:
|
||||
self.zk.lockNodeRequest(req, blocking=False)
|
||||
except exceptions.ZKLockException:
|
||||
continue
|
||||
|
||||
# Make sure the state didn't change on us after getting the lock
|
||||
req2 = self.zk.getNodeRequest(req_id)
|
||||
if req2 and req2.state != zk.REQUESTED:
|
||||
self.zk.unlockNodeRequest(req)
|
||||
continue
|
||||
|
||||
# Got a lock, so assign it
|
||||
self.log.info("Assigning node request %s" % req)
|
||||
rh = self._get_node_request_handler(provider, req)
|
||||
rh.run()
|
||||
if rh.paused:
|
||||
self.paused_handler = rh
|
||||
self.request_handlers.append(rh)
|
||||
|
||||
def _removeCompletedHandlers(self):
|
||||
'''
|
||||
Poll handlers to see which have completed.
|
||||
'''
|
||||
active_handlers = []
|
||||
for r in self.request_handlers:
|
||||
try:
|
||||
if not r.poll():
|
||||
active_handlers.append(r)
|
||||
else:
|
||||
self.log.debug("Removing handler for request %s",
|
||||
r.request.id)
|
||||
except Exception:
|
||||
# If we fail to poll a request handler log it but move on
|
||||
# and process the other handlers. We keep this handler around
|
||||
# and will try again later.
|
||||
self.log.exception("Error polling request handler for "
|
||||
"request %s", r.request.id)
|
||||
active_handlers.append(r)
|
||||
self.request_handlers = active_handlers
|
||||
active_reqs = [r.request.id for r in self.request_handlers]
|
||||
self.log.debug("Active requests: %s", active_reqs)
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# Public methods
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
def activeThreads(self):
|
||||
'''
|
||||
Return the number of alive threads in use by this provider.
|
||||
|
||||
This is an approximate, top-end number for alive threads, since some
|
||||
threads obviously may have finished by the time we finish the
|
||||
calculation.
|
||||
'''
|
||||
total = 0
|
||||
for r in self.request_handlers:
|
||||
total += r.alive_thread_count
|
||||
return total
|
||||
|
||||
def getZK(self):
|
||||
return self.nodepool.getZK()
|
||||
|
||||
def getProviderConfig(self):
|
||||
return self.nodepool.config.providers.get(self.provider_name)
|
||||
|
||||
def getPoolConfig(self):
|
||||
provider = self.getProviderConfig()
|
||||
if provider:
|
||||
return provider.pools[self.pool_name]
|
||||
else:
|
||||
return None
|
||||
|
||||
def getProviderManager(self):
|
||||
return self.nodepool.getProviderManager(self.provider_name)
|
||||
|
||||
def run(self):
|
||||
self.running = True
|
||||
|
||||
while self.running:
|
||||
# Don't do work if we've lost communication with the ZK cluster
|
||||
did_suspend = False
|
||||
while self.zk and (self.zk.suspended or self.zk.lost):
|
||||
did_suspend = True
|
||||
self.log.info("ZooKeeper suspended. Waiting")
|
||||
time.sleep(SUSPEND_WAIT_TIME)
|
||||
if did_suspend:
|
||||
self.log.info("ZooKeeper available. Resuming")
|
||||
|
||||
# Make sure we're always registered with ZK
|
||||
self.zk.registerLauncher(self.launcher_id)
|
||||
|
||||
try:
|
||||
if not self.paused_handler:
|
||||
self._assignHandlers()
|
||||
else:
|
||||
# If we are paused, one request handler could not
|
||||
# satisify its assigned request, so give it
|
||||
# another shot. Unpause ourselves if it completed.
|
||||
self.paused_handler.run()
|
||||
if not self.paused_handler.paused:
|
||||
self.paused_handler = None
|
||||
|
||||
self._removeCompletedHandlers()
|
||||
except Exception:
|
||||
self.log.exception("Error in PoolWorker:")
|
||||
time.sleep(self.watermark_sleep)
|
||||
|
||||
# Cleanup on exit
|
||||
if self.paused_handler:
|
||||
self.paused_handler.unlockNodeSet(clear_allocation=True)
|
||||
|
||||
def stop(self):
|
||||
'''
|
||||
Shutdown the PoolWorker thread.
|
||||
|
||||
Do not wait for the request handlers to finish. Any nodes
|
||||
that are in the process of launching will be cleaned up on a
|
||||
restart. They will be unlocked and BUILDING in ZooKeeper.
|
||||
'''
|
||||
self.log.info("%s received stop" % self.name)
|
||||
self.running = False
|
||||
|
||||
|
||||
class BaseCleanupWorker(threading.Thread):
|
||||
def __init__(self, nodepool, interval, name):
|
||||
threading.Thread.__init__(self, name=name)
|
||||
self._nodepool = nodepool
|
||||
self._interval = interval
|
||||
self._running = False
|
||||
|
||||
def _deleteInstance(self, node):
|
||||
'''
|
||||
Delete an instance from a provider.
|
||||
|
||||
A thread will be spawned to delete the actual instance from the
|
||||
provider.
|
||||
|
||||
:param Node node: A Node object representing the instance to delete.
|
||||
'''
|
||||
self.log.info("Deleting %s instance %s from %s",
|
||||
node.state, node.external_id, node.provider)
|
||||
try:
|
||||
t = NodeDeleter(
|
||||
self._nodepool.getZK(),
|
||||
self._nodepool.getProviderManager(node.provider),
|
||||
node)
|
||||
t.start()
|
||||
except Exception:
|
||||
self.log.exception("Could not delete instance %s on provider %s",
|
||||
node.external_id, node.provider)
|
||||
|
||||
def run(self):
|
||||
self.log.info("Starting")
|
||||
self._running = True
|
||||
|
||||
while self._running:
|
||||
# Don't do work if we've lost communication with the ZK cluster
|
||||
did_suspend = False
|
||||
zk_conn = self._nodepool.getZK()
|
||||
while zk_conn and (zk_conn.suspended or zk_conn.lost):
|
||||
did_suspend = True
|
||||
self.log.info("ZooKeeper suspended. Waiting")
|
||||
time.sleep(SUSPEND_WAIT_TIME)
|
||||
if did_suspend:
|
||||
self.log.info("ZooKeeper available. Resuming")
|
||||
|
||||
self._run()
|
||||
time.sleep(self._interval)
|
||||
|
||||
self.log.info("Stopped")
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
self.join()
|
||||
|
||||
|
||||
class CleanupWorker(BaseCleanupWorker):
|
||||
def __init__(self, nodepool, interval):
|
||||
super(CleanupWorker, self).__init__(
|
||||
nodepool, interval, name='CleanupWorker')
|
||||
self.log = logging.getLogger("nodepool.CleanupWorker")
|
||||
|
||||
def _resetLostRequest(self, zk_conn, req):
|
||||
'''
|
||||
Reset the request state and deallocate nodes.
|
||||
|
||||
:param ZooKeeper zk_conn: A ZooKeeper connection object.
|
||||
:param NodeRequest req: The lost NodeRequest object.
|
||||
'''
|
||||
# Double check the state after the lock
|
||||
req = zk_conn.getNodeRequest(req.id)
|
||||
if req.state != zk.PENDING:
|
||||
return
|
||||
|
||||
for node in zk_conn.nodeIterator():
|
||||
if node.allocated_to == req.id:
|
||||
try:
|
||||
zk_conn.lockNode(node)
|
||||
except exceptions.ZKLockException:
|
||||
self.log.warning(
|
||||
"Unable to grab lock to deallocate node %s from "
|
||||
"request %s", node.id, req.id)
|
||||
return
|
||||
|
||||
node.allocated_to = None
|
||||
try:
|
||||
zk_conn.storeNode(node)
|
||||
self.log.debug("Deallocated node %s for lost request %s",
|
||||
node.id, req.id)
|
||||
except Exception:
|
||||
self.log.exception(
|
||||
"Unable to deallocate node %s from request %s:",
|
||||
node.id, req.id)
|
||||
|
||||
zk_conn.unlockNode(node)
|
||||
|
||||
req.state = zk.REQUESTED
|
||||
req.nodes = []
|
||||
zk_conn.storeNodeRequest(req)
|
||||
self.log.info("Reset lost request %s", req.id)
|
||||
|
||||
def _cleanupLostRequests(self):
|
||||
'''
|
||||
Look for lost requests and reset them.
|
||||
|
||||
A lost request is a node request that was left in the PENDING state
|
||||
when nodepool exited. We need to look for these (they'll be unlocked)
|
||||
and disassociate any nodes we've allocated to the request and reset
|
||||
the request state to REQUESTED so it will be processed again.
|
||||
'''
|
||||
zk_conn = self._nodepool.getZK()
|
||||
for req in zk_conn.nodeRequestIterator():
|
||||
if req.state == zk.PENDING:
|
||||
try:
|
||||
zk_conn.lockNodeRequest(req, blocking=False)
|
||||
except exceptions.ZKLockException:
|
||||
continue
|
||||
|
||||
try:
|
||||
self._resetLostRequest(zk_conn, req)
|
||||
except Exception:
|
||||
self.log.exception("Error resetting lost request %s:",
|
||||
req.id)
|
||||
|
||||
zk_conn.unlockNodeRequest(req)
|
||||
|
||||
def _cleanupNodeRequestLocks(self):
|
||||
'''
|
||||
Remove request locks where the request no longer exists.
|
||||
|
||||
Because the node request locks are not direct children of the request
|
||||
znode, we need to remove the locks separately after the request has
|
||||
been processed. Only remove them after LOCK_CLEANUP seconds have
|
||||
passed. This helps reduce chances of the scenario where a request could
|
||||
go away _while_ a lock is currently held for processing and the cleanup
|
||||
thread attempts to delete it. The delay should reduce the chance that
|
||||
we delete a currently held lock.
|
||||
'''
|
||||
zk = self._nodepool.getZK()
|
||||
requests = zk.getNodeRequests()
|
||||
now = time.time()
|
||||
for lock_stat in zk.nodeRequestLockStatsIterator():
|
||||
if lock_stat.lock_id in requests:
|
||||
continue
|
||||
if (now - lock_stat.stat.mtime / 1000) > LOCK_CLEANUP:
|
||||
zk.deleteNodeRequestLock(lock_stat.lock_id)
|
||||
|
||||
def _cleanupLeakedInstances(self):
|
||||
'''
|
||||
Delete any leaked server instances.
|
||||
|
||||
Remove any servers we find in providers we know about that are not
|
||||
recorded in the ZooKeeper data.
|
||||
'''
|
||||
zk_conn = self._nodepool.getZK()
|
||||
|
||||
for provider in self._nodepool.config.providers.values():
|
||||
manager = self._nodepool.getProviderManager(provider.name)
|
||||
|
||||
for server in manager.listNodes():
|
||||
meta = server.get('metadata', {})
|
||||
|
||||
if 'nodepool_provider_name' not in meta:
|
||||
continue
|
||||
|
||||
if meta['nodepool_provider_name'] != provider.name:
|
||||
# Another launcher, sharing this provider but configured
|
||||
# with a different name, owns this.
|
||||
continue
|
||||
|
||||
if not zk_conn.getNode(meta['nodepool_node_id']):
|
||||
self.log.warning(
|
||||
"Deleting leaked instance %s (%s) in %s "
|
||||
"(unknown node id %s)",
|
||||
server.name, server.id, provider.name,
|
||||
meta['nodepool_node_id']
|
||||
)
|
||||
# Create an artifical node to use for deleting the server.
|
||||
node = zk.Node()
|
||||
node.external_id = server.id
|
||||
node.provider = provider.name
|
||||
self._deleteInstance(node)
|
||||
|
||||
manager.cleanupLeakedResources()
|
||||
|
||||
def _cleanupMaxReadyAge(self):
|
||||
'''
|
||||
Delete any server past their max-ready-age.
|
||||
|
||||
Remove any servers which are longer than max-ready-age in ready state.
|
||||
'''
|
||||
|
||||
# first get all labels with max_ready_age > 0
|
||||
label_names = []
|
||||
for label_name in self._nodepool.config.labels:
|
||||
if self._nodepool.config.labels[label_name].max_ready_age > 0:
|
||||
label_names.append(label_name)
|
||||
|
||||
zk_conn = self._nodepool.getZK()
|
||||
ready_nodes = zk_conn.getReadyNodesOfTypes(label_names)
|
||||
|
||||
for label_name in ready_nodes:
|
||||
# get label from node
|
||||
label = self._nodepool.config.labels[label_name]
|
||||
|
||||
for node in ready_nodes[label_name]:
|
||||
|
||||
# Can't do anything if we aren't configured for this provider.
|
||||
if node.provider not in self._nodepool.config.providers:
|
||||
continue
|
||||
|
||||
# check state time against now
|
||||
now = int(time.time())
|
||||
if (now - node.state_time) < label.max_ready_age:
|
||||
continue
|
||||
|
||||
try:
|
||||
zk_conn.lockNode(node, blocking=False)
|
||||
except exceptions.ZKLockException:
|
||||
continue
|
||||
|
||||
# Double check the state now that we have a lock since it
|
||||
# may have changed on us.
|
||||
if node.state != zk.READY:
|
||||
zk_conn.unlockNode(node)
|
||||
continue
|
||||
|
||||
self.log.debug("Node %s exceeds max ready age: %s >= %s",
|
||||
node.id, now - node.state_time,
|
||||
label.max_ready_age)
|
||||
|
||||
# The NodeDeleter thread will unlock and remove the
|
||||
# node from ZooKeeper if it succeeds.
|
||||
try:
|
||||
self._deleteInstance(node)
|
||||
except Exception:
|
||||
self.log.exception("Failure deleting aged node %s:",
|
||||
node.id)
|
||||
zk_conn.unlockNode(node)
|
||||
|
||||
def _run(self):
|
||||
'''
|
||||
Catch exceptions individually so that other cleanup routines may
|
||||
have a chance.
|
||||
'''
|
||||
try:
|
||||
self._cleanupNodeRequestLocks()
|
||||
except Exception:
|
||||
self.log.exception(
|
||||
"Exception in CleanupWorker (node request lock cleanup):")
|
||||
|
||||
try:
|
||||
self._cleanupLeakedInstances()
|
||||
except Exception:
|
||||
self.log.exception(
|
||||
"Exception in CleanupWorker (leaked instance cleanup):")
|
||||
|
||||
try:
|
||||
self._cleanupLostRequests()
|
||||
except Exception:
|
||||
self.log.exception(
|
||||
"Exception in CleanupWorker (lost request cleanup):")
|
||||
|
||||
try:
|
||||
self._cleanupMaxReadyAge()
|
||||
except Exception:
|
||||
self.log.exception(
|
||||
"Exception in CleanupWorker (max ready age cleanup):")
|
||||
|
||||
|
||||
class DeletedNodeWorker(BaseCleanupWorker):
|
||||
def __init__(self, nodepool, interval):
|
||||
super(DeletedNodeWorker, self).__init__(
|
||||
nodepool, interval, name='DeletedNodeWorker')
|
||||
self.log = logging.getLogger("nodepool.DeletedNodeWorker")
|
||||
|
||||
def _cleanupNodes(self):
|
||||
'''
|
||||
Delete instances from providers and nodes entries from ZooKeeper.
|
||||
'''
|
||||
cleanup_states = (zk.USED, zk.IN_USE, zk.BUILDING, zk.FAILED,
|
||||
zk.DELETING)
|
||||
|
||||
zk_conn = self._nodepool.getZK()
|
||||
for node in zk_conn.nodeIterator():
|
||||
# If a ready node has been allocated to a request, but that
|
||||
# request is now missing, deallocate it.
|
||||
if (node.state == zk.READY and node.allocated_to
|
||||
and not zk_conn.getNodeRequest(node.allocated_to)):
|
||||
try:
|
||||
zk_conn.lockNode(node, blocking=False)
|
||||
except exceptions.ZKLockException:
|
||||
pass
|
||||
else:
|
||||
# Double check node conditions after lock
|
||||
if node.state == zk.READY and node.allocated_to:
|
||||
node.allocated_to = None
|
||||
try:
|
||||
zk_conn.storeNode(node)
|
||||
self.log.debug(
|
||||
"Deallocated node %s with missing request %s",
|
||||
node.id, node.allocated_to)
|
||||
except Exception:
|
||||
self.log.exception(
|
||||
"Failed to deallocate node %s for missing "
|
||||
"request %s:", node.id, node.allocated_to)
|
||||
|
||||
zk_conn.unlockNode(node)
|
||||
|
||||
# Can't do anything if we aren't configured for this provider.
|
||||
if node.provider not in self._nodepool.config.providers:
|
||||
continue
|
||||
|
||||
# Any nodes in these states that are unlocked can be deleted.
|
||||
if node.state in cleanup_states:
|
||||
try:
|
||||
zk_conn.lockNode(node, blocking=False)
|
||||
except exceptions.ZKLockException:
|
||||
continue
|
||||
|
||||
# Double check the state now that we have a lock since it
|
||||
# may have changed on us.
|
||||
if node.state not in cleanup_states:
|
||||
zk_conn.unlockNode(node)
|
||||
continue
|
||||
|
||||
self.log.debug(
|
||||
"Marking for deletion unlocked node %s "
|
||||
"(state: %s, allocated_to: %s)",
|
||||
node.id, node.state, node.allocated_to)
|
||||
|
||||
# The NodeDeleter thread will unlock and remove the
|
||||
# node from ZooKeeper if it succeeds.
|
||||
try:
|
||||
self._deleteInstance(node)
|
||||
except Exception:
|
||||
self.log.exception(
|
||||
"Failure deleting node %s in cleanup state %s:",
|
||||
node.id, node.state)
|
||||
zk_conn.unlockNode(node)
|
||||
|
||||
def _run(self):
|
||||
try:
|
||||
self._cleanupNodes()
|
||||
except Exception:
|
||||
self.log.exception("Exception in DeletedNodeWorker:")
|
||||
|
||||
|
||||
class NodePool(threading.Thread):
|
||||
log = logging.getLogger("nodepool.NodePool")
|
||||
|
||||
def __init__(self, securefile, configfile,
|
||||
watermark_sleep=WATERMARK_SLEEP):
|
||||
threading.Thread.__init__(self, name='NodePool')
|
||||
self.securefile = securefile
|
||||
self.configfile = configfile
|
||||
self.watermark_sleep = watermark_sleep
|
||||
self.cleanup_interval = 60
|
||||
self.delete_interval = 5
|
||||
self._stopped = False
|
||||
self.config = None
|
||||
self.zk = None
|
||||
self.statsd = stats.get_client()
|
||||
self._pool_threads = {}
|
||||
self._cleanup_thread = None
|
||||
self._delete_thread = None
|
||||
self._wake_condition = threading.Condition()
|
||||
self._submittedRequests = {}
|
||||
|
||||
def stop(self):
|
||||
self._stopped = True
|
||||
self._wake_condition.acquire()
|
||||
self._wake_condition.notify()
|
||||
self._wake_condition.release()
|
||||
if self.config:
|
||||
provider_manager.ProviderManager.stopProviders(self.config)
|
||||
|
||||
if self._cleanup_thread:
|
||||
self._cleanup_thread.stop()
|
||||
self._cleanup_thread.join()
|
||||
|
||||
if self._delete_thread:
|
||||
self._delete_thread.stop()
|
||||
self._delete_thread.join()
|
||||
|
||||
# Don't let stop() return until all pool threads have been
|
||||
# terminated.
|
||||
self.log.debug("Stopping pool threads")
|
||||
for thd in self._pool_threads.values():
|
||||
if thd.isAlive():
|
||||
thd.stop()
|
||||
self.log.debug("Waiting for %s" % thd.name)
|
||||
thd.join()
|
||||
|
||||
if self.isAlive():
|
||||
self.join()
|
||||
if self.zk:
|
||||
self.zk.disconnect()
|
||||
self.log.debug("Finished stopping")
|
||||
|
||||
def loadConfig(self):
|
||||
config = nodepool_config.loadConfig(self.configfile)
|
||||
if self.securefile:
|
||||
nodepool_config.loadSecureConfig(config, self.securefile)
|
||||
return config
|
||||
|
||||
def reconfigureZooKeeper(self, config):
|
||||
if self.config:
|
||||
running = list(self.config.zookeeper_servers.values())
|
||||
else:
|
||||
running = None
|
||||
|
||||
configured = list(config.zookeeper_servers.values())
|
||||
if running == configured:
|
||||
return
|
||||
|
||||
if not self.zk and configured:
|
||||
self.log.debug("Connecting to ZooKeeper servers")
|
||||
self.zk = zk.ZooKeeper()
|
||||
self.zk.connect(configured)
|
||||
else:
|
||||
self.log.debug("Detected ZooKeeper server changes")
|
||||
self.zk.resetHosts(configured)
|
||||
|
||||
def setConfig(self, config):
|
||||
self.config = config
|
||||
|
||||
def getZK(self):
|
||||
return self.zk
|
||||
|
||||
def getProviderManager(self, provider_name):
|
||||
return self.config.provider_managers[provider_name]
|
||||
|
||||
def getPoolWorkers(self, provider_name):
|
||||
return [t for t in self._pool_threads.values() if
|
||||
t.provider_name == provider_name]
|
||||
|
||||
def updateConfig(self):
|
||||
config = self.loadConfig()
|
||||
provider_manager.ProviderManager.reconfigure(self.config, config)
|
||||
self.reconfigureZooKeeper(config)
|
||||
self.setConfig(config)
|
||||
|
||||
def removeCompletedRequests(self):
|
||||
'''
|
||||
Remove (locally and in ZK) fulfilled node requests.
|
||||
|
||||
We also must reset the allocated_to attribute for each Node assigned
|
||||
to our request, since we are deleting the request.
|
||||
'''
|
||||
|
||||
# Use a copy of the labels because we modify _submittedRequests
|
||||
# within the loop below. Note that keys() returns an iterator in
|
||||
# py3, so we need to explicitly make a new list.
|
||||
requested_labels = list(self._submittedRequests.keys())
|
||||
|
||||
for label in requested_labels:
|
||||
label_requests = self._submittedRequests[label]
|
||||
active_requests = []
|
||||
|
||||
for req in label_requests:
|
||||
req = self.zk.getNodeRequest(req.id)
|
||||
|
||||
if not req:
|
||||
continue
|
||||
|
||||
if req.state == zk.FULFILLED:
|
||||
# Reset node allocated_to
|
||||
for node_id in req.nodes:
|
||||
node = self.zk.getNode(node_id)
|
||||
node.allocated_to = None
|
||||
# NOTE: locking shouldn't be necessary since a node
|
||||
# with allocated_to set should not be locked except
|
||||
# by the creator of the request (us).
|
||||
self.zk.storeNode(node)
|
||||
self.zk.deleteNodeRequest(req)
|
||||
elif req.state == zk.FAILED:
|
||||
self.log.debug("min-ready node request failed: %s", req)
|
||||
self.zk.deleteNodeRequest(req)
|
||||
else:
|
||||
active_requests.append(req)
|
||||
|
||||
if active_requests:
|
||||
self._submittedRequests[label] = active_requests
|
||||
else:
|
||||
self.log.debug(
|
||||
"No more active min-ready requests for label %s", label)
|
||||
del self._submittedRequests[label]
|
||||
|
||||
def labelImageIsAvailable(self, label):
|
||||
'''
|
||||
Check if the image associated with a label is ready in any provider.
|
||||
|
||||
:param Label label: The label config object.
|
||||
|
||||
:returns: True if image associated with the label is uploaded and
|
||||
ready in at least one provider. False otherwise.
|
||||
'''
|
||||
for pool in label.pools:
|
||||
if not pool.provider.driver.manage_images:
|
||||
# Provider doesn't manage images, assuming label is ready
|
||||
return True
|
||||
for pool_label in pool.labels.values():
|
||||
if pool_label.diskimage:
|
||||
if self.zk.getMostRecentImageUpload(
|
||||
pool_label.diskimage.name, pool.provider.name):
|
||||
return True
|
||||
else:
|
||||
manager = self.getProviderManager(pool.provider.name)
|
||||
if manager.labelReady(pool_label):
|
||||
return True
|
||||
return False
|
||||
|
||||
def createMinReady(self):
|
||||
'''
|
||||
Create node requests to make the minimum amount of ready nodes.
|
||||
|
||||
Since this method will be called repeatedly, we need to take care to
|
||||
note when we have already submitted node requests to satisfy min-ready.
|
||||
Requests we've already submitted are stored in the _submittedRequests
|
||||
dict, keyed by label.
|
||||
'''
|
||||
def createRequest(label_name):
|
||||
req = zk.NodeRequest()
|
||||
req.state = zk.REQUESTED
|
||||
req.requestor = "NodePool:min-ready"
|
||||
req.node_types.append(label_name)
|
||||
req.reuse = False # force new node launches
|
||||
self.zk.storeNodeRequest(req, priority="100")
|
||||
if label_name not in self._submittedRequests:
|
||||
self._submittedRequests[label_name] = []
|
||||
self._submittedRequests[label_name].append(req)
|
||||
|
||||
# Since we could have already submitted node requests, do not
|
||||
# resubmit a request for a type if a request for that type is
|
||||
# still in progress.
|
||||
self.removeCompletedRequests()
|
||||
label_names = list(self.config.labels.keys())
|
||||
requested_labels = list(self._submittedRequests.keys())
|
||||
needed_labels = list(set(label_names) - set(requested_labels))
|
||||
|
||||
ready_nodes = self.zk.getReadyNodesOfTypes(needed_labels)
|
||||
|
||||
for label in self.config.labels.values():
|
||||
if label.name not in needed_labels:
|
||||
continue
|
||||
min_ready = label.min_ready
|
||||
if min_ready == -1:
|
||||
continue # disabled
|
||||
|
||||
# Calculate how many nodes of this type we need created
|
||||
need = 0
|
||||
if label.name not in ready_nodes:
|
||||
need = label.min_ready
|
||||
elif len(ready_nodes[label.name]) < min_ready:
|
||||
need = min_ready - len(ready_nodes[label.name])
|
||||
|
||||
if need and self.labelImageIsAvailable(label):
|
||||
# Create requests for 1 node at a time. This helps to split
|
||||
# up requests across providers, and avoids scenario where a
|
||||
# single provider might fail the entire request because of
|
||||
# quota (e.g., min-ready=2, but max-servers=1).
|
||||
self.log.info("Creating requests for %d %s nodes",
|
||||
need, label.name)
|
||||
for i in range(0, need):
|
||||
createRequest(label.name)
|
||||
|
||||
def run(self):
|
||||
'''
|
||||
Start point for the NodePool thread.
|
||||
'''
|
||||
while not self._stopped:
|
||||
try:
|
||||
self.updateConfig()
|
||||
|
||||
# Don't do work if we've lost communication with the ZK cluster
|
||||
did_suspend = False
|
||||
while self.zk and (self.zk.suspended or self.zk.lost):
|
||||
did_suspend = True
|
||||
self.log.info("ZooKeeper suspended. Waiting")
|
||||
time.sleep(SUSPEND_WAIT_TIME)
|
||||
if did_suspend:
|
||||
self.log.info("ZooKeeper available. Resuming")
|
||||
|
||||
self.createMinReady()
|
||||
|
||||
if not self._cleanup_thread:
|
||||
self._cleanup_thread = CleanupWorker(
|
||||
self, self.cleanup_interval)
|
||||
self._cleanup_thread.start()
|
||||
|
||||
if not self._delete_thread:
|
||||
self._delete_thread = DeletedNodeWorker(
|
||||
self, self.delete_interval)
|
||||
self._delete_thread.start()
|
||||
|
||||
# Stop any PoolWorker threads if the pool was removed
|
||||
# from the config.
|
||||
pool_keys = set()
|
||||
for provider in self.config.providers.values():
|
||||
for pool in provider.pools.values():
|
||||
pool_keys.add(provider.name + '-' + pool.name)
|
||||
|
||||
new_pool_threads = {}
|
||||
for key in self._pool_threads.keys():
|
||||
if key not in pool_keys:
|
||||
self._pool_threads[key].stop()
|
||||
else:
|
||||
new_pool_threads[key] = self._pool_threads[key]
|
||||
self._pool_threads = new_pool_threads
|
||||
|
||||
# Start (or restart) provider threads for each provider in
|
||||
# the config. Removing a provider from the config and then
|
||||
# adding it back would cause a restart.
|
||||
for provider in self.config.providers.values():
|
||||
for pool in provider.pools.values():
|
||||
key = provider.name + '-' + pool.name
|
||||
if key not in self._pool_threads:
|
||||
t = PoolWorker(self, provider.name, pool.name)
|
||||
self.log.info("Starting %s" % t.name)
|
||||
t.start()
|
||||
self._pool_threads[key] = t
|
||||
elif not self._pool_threads[key].isAlive():
|
||||
self._pool_threads[key].join()
|
||||
t = PoolWorker(self, provider.name, pool.name)
|
||||
self.log.info("Restarting %s" % t.name)
|
||||
t.start()
|
||||
self._pool_threads[key] = t
|
||||
except Exception:
|
||||
self.log.exception("Exception in main loop:")
|
||||
|
||||
self._wake_condition.acquire()
|
||||
self._wake_condition.wait(self.watermark_sleep)
|
||||
self._wake_condition.release()
|
@ -1,319 +0,0 @@
|
||||
# Copyright (C) 2011-2014 OpenStack Foundation
|
||||
#
|
||||
# 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 time
|
||||
|
||||
# States:
|
||||
# The cloud provider is building this machine. We have an ID, but it's
|
||||
# not ready for use.
|
||||
BUILDING = 1
|
||||
# The machine is ready for use.
|
||||
READY = 2
|
||||
# This can mean in-use, or used but complete.
|
||||
USED = 3
|
||||
# Delete this machine immediately.
|
||||
DELETE = 4
|
||||
# Keep this machine indefinitely.
|
||||
HOLD = 5
|
||||
# Acceptance testing (pre-ready)
|
||||
TEST = 6
|
||||
|
||||
|
||||
STATE_NAMES = {
|
||||
BUILDING: 'building',
|
||||
READY: 'ready',
|
||||
USED: 'used',
|
||||
DELETE: 'delete',
|
||||
HOLD: 'hold',
|
||||
TEST: 'test',
|
||||
}
|
||||
|
||||
from sqlalchemy import Table, Column, Integer, String, \
|
||||
MetaData, create_engine
|
||||
from sqlalchemy.orm import scoped_session, mapper, relationship, foreign
|
||||
from sqlalchemy.orm.session import Session, sessionmaker
|
||||
|
||||
metadata = MetaData()
|
||||
|
||||
node_table = Table(
|
||||
'node', metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('provider_name', String(255), index=True, nullable=False),
|
||||
Column('label_name', String(255), index=True, nullable=False),
|
||||
Column('target_name', String(255), index=True, nullable=False),
|
||||
Column('manager_name', String(255)),
|
||||
# Machine name
|
||||
Column('hostname', String(255), index=True),
|
||||
# Eg, jenkins node name
|
||||
Column('nodename', String(255), index=True),
|
||||
# Provider assigned id for this machine
|
||||
Column('external_id', String(255)),
|
||||
# Provider availability zone for this machine
|
||||
Column('az', String(255)),
|
||||
# Primary IP address
|
||||
Column('ip', String(255)),
|
||||
# Internal/fixed IP address
|
||||
Column('ip_private', String(255)),
|
||||
# One of the above values
|
||||
Column('state', Integer),
|
||||
# Time of last state change
|
||||
Column('state_time', Integer),
|
||||
# Comment about the state of the node - used to annotate held nodes
|
||||
Column('comment', String(255)),
|
||||
mysql_engine='InnoDB',
|
||||
)
|
||||
subnode_table = Table(
|
||||
'subnode', metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('node_id', Integer, index=True, nullable=False),
|
||||
# Machine name
|
||||
Column('hostname', String(255), index=True),
|
||||
# Provider assigned id for this machine
|
||||
Column('external_id', String(255)),
|
||||
# Primary IP address
|
||||
Column('ip', String(255)),
|
||||
# Internal/fixed IP address
|
||||
Column('ip_private', String(255)),
|
||||
# One of the above values
|
||||
Column('state', Integer),
|
||||
# Time of last state change
|
||||
Column('state_time', Integer),
|
||||
mysql_engine='InnoDB',
|
||||
)
|
||||
job_table = Table(
|
||||
'job', metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
# The name of the job
|
||||
Column('name', String(255), index=True),
|
||||
# Automatically hold up to this number of nodes that fail this job
|
||||
Column('hold_on_failure', Integer),
|
||||
mysql_engine='InnoDB',
|
||||
)
|
||||
|
||||
|
||||
class Node(object):
|
||||
def __init__(self, provider_name, label_name, target_name, az,
|
||||
hostname=None, external_id=None, ip=None, ip_private=None,
|
||||
manager_name=None, state=BUILDING, comment=None):
|
||||
self.provider_name = provider_name
|
||||
self.label_name = label_name
|
||||
self.target_name = target_name
|
||||
self.manager_name = manager_name
|
||||
self.external_id = external_id
|
||||
self.az = az
|
||||
self.ip = ip
|
||||
self.ip_private = ip_private
|
||||
self.hostname = hostname
|
||||
self.state = state
|
||||
self.comment = comment
|
||||
|
||||
def delete(self):
|
||||
session = Session.object_session(self)
|
||||
session.delete(self)
|
||||
session.commit()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
@state.setter
|
||||
def state(self, state):
|
||||
self._state = state
|
||||
self.state_time = int(time.time())
|
||||
session = Session.object_session(self)
|
||||
if session:
|
||||
session.commit()
|
||||
|
||||
|
||||
class SubNode(object):
|
||||
def __init__(self, node,
|
||||
hostname=None, external_id=None, ip=None, ip_private=None,
|
||||
state=BUILDING):
|
||||
self.node_id = node.id
|
||||
self.provider_name = node.provider_name
|
||||
self.label_name = node.label_name
|
||||
self.target_name = node.target_name
|
||||
self.external_id = external_id
|
||||
self.ip = ip
|
||||
self.ip_private = ip_private
|
||||
self.hostname = hostname
|
||||
self.state = state
|
||||
|
||||
def delete(self):
|
||||
session = Session.object_session(self)
|
||||
session.delete(self)
|
||||
session.commit()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
@state.setter
|
||||
def state(self, state):
|
||||
self._state = state
|
||||
self.state_time = int(time.time())
|
||||
session = Session.object_session(self)
|
||||
if session:
|
||||
session.commit()
|
||||
|
||||
|
||||
class Job(object):
|
||||
def __init__(self, name=None, hold_on_failure=0):
|
||||
self.name = name
|
||||
self.hold_on_failure = hold_on_failure
|
||||
|
||||
def delete(self):
|
||||
session = Session.object_session(self)
|
||||
session.delete(self)
|
||||
session.commit()
|
||||
|
||||
|
||||
mapper(Job, job_table)
|
||||
|
||||
|
||||
mapper(SubNode, subnode_table,
|
||||
properties=dict(_state=subnode_table.c.state))
|
||||
|
||||
|
||||
mapper(Node, node_table,
|
||||
properties=dict(
|
||||
_state=node_table.c.state,
|
||||
subnodes=relationship(
|
||||
SubNode,
|
||||
cascade='all, delete-orphan',
|
||||
uselist=True,
|
||||
primaryjoin=foreign(subnode_table.c.node_id) == node_table.c.id,
|
||||
backref='node')))
|
||||
|
||||
|
||||
class NodeDatabase(object):
|
||||
def __init__(self, dburi):
|
||||
engine_kwargs = dict(echo=False, pool_recycle=3600)
|
||||
if 'sqlite:' not in dburi:
|
||||
engine_kwargs['max_overflow'] = -1
|
||||
|
||||
self.engine = create_engine(dburi, **engine_kwargs)
|
||||
metadata.create_all(self.engine)
|
||||
self.session_factory = sessionmaker(bind=self.engine)
|
||||
self.session = scoped_session(self.session_factory)
|
||||
|
||||
def getSession(self):
|
||||
return NodeDatabaseSession(self.session)
|
||||
|
||||
|
||||
class NodeDatabaseSession(object):
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, etype, value, tb):
|
||||
if etype:
|
||||
self.session().rollback()
|
||||
else:
|
||||
self.session().commit()
|
||||
self.session().close()
|
||||
self.session = None
|
||||
|
||||
def abort(self):
|
||||
self.session().rollback()
|
||||
|
||||
def commit(self):
|
||||
self.session().commit()
|
||||
|
||||
def delete(self, obj):
|
||||
self.session().delete(obj)
|
||||
|
||||
def getNodes(self, provider_name=None, label_name=None, target_name=None,
|
||||
state=None):
|
||||
exp = self.session().query(Node).order_by(
|
||||
node_table.c.provider_name,
|
||||
node_table.c.label_name)
|
||||
if provider_name:
|
||||
exp = exp.filter_by(provider_name=provider_name)
|
||||
if label_name:
|
||||
exp = exp.filter_by(label_name=label_name)
|
||||
if target_name:
|
||||
exp = exp.filter_by(target_name=target_name)
|
||||
if state:
|
||||
exp = exp.filter(node_table.c.state == state)
|
||||
return exp.all()
|
||||
|
||||
def createNode(self, *args, **kwargs):
|
||||
new = Node(*args, **kwargs)
|
||||
self.session().add(new)
|
||||
self.commit()
|
||||
return new
|
||||
|
||||
def createSubNode(self, *args, **kwargs):
|
||||
new = SubNode(*args, **kwargs)
|
||||
self.session().add(new)
|
||||
self.commit()
|
||||
return new
|
||||
|
||||
def getNode(self, id):
|
||||
nodes = self.session().query(Node).filter_by(id=id).all()
|
||||
if not nodes:
|
||||
return None
|
||||
return nodes[0]
|
||||
|
||||
def getSubNode(self, id):
|
||||
nodes = self.session().query(SubNode).filter_by(id=id).all()
|
||||
if not nodes:
|
||||
return None
|
||||
return nodes[0]
|
||||
|
||||
def getNodeByHostname(self, hostname):
|
||||
nodes = self.session().query(Node).filter_by(hostname=hostname).all()
|
||||
if not nodes:
|
||||
return None
|
||||
return nodes[0]
|
||||
|
||||
def getNodeByNodename(self, nodename):
|
||||
nodes = self.session().query(Node).filter_by(nodename=nodename).all()
|
||||
if not nodes:
|
||||
return None
|
||||
return nodes[0]
|
||||
|
||||
def getNodeByExternalID(self, provider_name, external_id):
|
||||
nodes = self.session().query(Node).filter_by(
|
||||
provider_name=provider_name,
|
||||
external_id=external_id).all()
|
||||
if not nodes:
|
||||
return None
|
||||
return nodes[0]
|
||||
|
||||
def getJob(self, id):
|
||||
jobs = self.session().query(Job).filter_by(id=id).all()
|
||||
if not jobs:
|
||||
return None
|
||||
return jobs[0]
|
||||
|
||||
def getJobByName(self, name):
|
||||
jobs = self.session().query(Job).filter_by(name=name).all()
|
||||
if not jobs:
|
||||
return None
|
||||
return jobs[0]
|
||||
|
||||
def getJobs(self):
|
||||
return self.session().query(Job).all()
|
||||
|
||||
def createJob(self, *args, **kwargs):
|
||||
new = Job(*args, **kwargs)
|
||||
self.session().add(new)
|
||||
self.commit()
|
||||
return new
|
1735
nodepool/nodepool.py
1735
nodepool/nodepool.py
File diff suppressed because it is too large
Load Diff
78
nodepool/nodeutils.py
Normal file → Executable file
78
nodepool/nodeutils.py
Normal file → Executable file
@ -17,21 +17,20 @@
|
||||
# limitations under the License.
|
||||
|
||||
import errno
|
||||
import ipaddress
|
||||
import time
|
||||
import six
|
||||
import socket
|
||||
import logging
|
||||
from sshclient import SSHClient
|
||||
|
||||
import fakeprovider
|
||||
import paramiko
|
||||
|
||||
import exceptions
|
||||
from nodepool import exceptions
|
||||
|
||||
log = logging.getLogger("nodepool.utils")
|
||||
|
||||
|
||||
ITERATE_INTERVAL = 2 # How long to sleep while waiting for something
|
||||
# in a loop
|
||||
# How long to sleep while waiting for something in a loop
|
||||
ITERATE_INTERVAL = 2
|
||||
|
||||
|
||||
def iterate_timeout(max_seconds, exc, purpose):
|
||||
@ -44,32 +43,57 @@ def iterate_timeout(max_seconds, exc, purpose):
|
||||
raise exc("Timeout waiting for %s" % purpose)
|
||||
|
||||
|
||||
def ssh_connect(ip, username, connect_kwargs={}, timeout=60):
|
||||
def keyscan(ip, port=22, timeout=60):
|
||||
'''
|
||||
Scan the IP address for public SSH keys.
|
||||
|
||||
Keys are returned formatted as: "<type> <base64_string>"
|
||||
'''
|
||||
if 'fake' in ip:
|
||||
return fakeprovider.FakeSSHClient()
|
||||
# HPcloud may return ECONNREFUSED or EHOSTUNREACH
|
||||
# for about 30 seconds after adding the IP
|
||||
return ['ssh-rsa FAKEKEY']
|
||||
|
||||
if ipaddress.ip_address(six.text_type(ip)).version < 6:
|
||||
family = socket.AF_INET
|
||||
sockaddr = (ip, port)
|
||||
else:
|
||||
family = socket.AF_INET6
|
||||
sockaddr = (ip, port, 0, 0)
|
||||
|
||||
keys = []
|
||||
key = None
|
||||
for count in iterate_timeout(
|
||||
timeout, exceptions.SSHTimeoutException, "ssh access"):
|
||||
sock = None
|
||||
t = None
|
||||
try:
|
||||
client = SSHClient(ip, username, **connect_kwargs)
|
||||
sock = socket.socket(family, socket.SOCK_STREAM)
|
||||
sock.settimeout(timeout)
|
||||
sock.connect(sockaddr)
|
||||
t = paramiko.transport.Transport(sock)
|
||||
t.start_client(timeout=timeout)
|
||||
key = t.get_remote_server_key()
|
||||
break
|
||||
except paramiko.SSHException as e:
|
||||
# NOTE(pabelanger): Currently paramiko only returns a string with
|
||||
# error code. If we want finer granularity we'll need to regex the
|
||||
# string.
|
||||
log.exception('Failed to negotiate SSH: %s' % (e))
|
||||
except paramiko.AuthenticationException as e:
|
||||
# This covers the case where the cloud user is created
|
||||
# after sshd is up (Fedora for example)
|
||||
log.info('Auth exception for %s@%s. Try number %i...' %
|
||||
(username, ip, count))
|
||||
except socket.error as e:
|
||||
if e[0] not in [errno.ECONNREFUSED, errno.EHOSTUNREACH, None]:
|
||||
if e.errno not in [errno.ECONNREFUSED, errno.EHOSTUNREACH, None]:
|
||||
log.exception(
|
||||
'Exception while testing ssh access to %s:' % ip)
|
||||
'Exception with ssh access to %s:' % ip)
|
||||
except Exception as e:
|
||||
log.exception("ssh-keyscan failure: %s", e)
|
||||
finally:
|
||||
try:
|
||||
if t:
|
||||
t.close()
|
||||
except Exception as e:
|
||||
log.exception('Exception closing paramiko: %s', e)
|
||||
try:
|
||||
if sock:
|
||||
sock.close()
|
||||
except Exception as e:
|
||||
log.exception('Exception closing socket: %s', e)
|
||||
|
||||
out = client.ssh("test ssh access", "echo access okay", output=True)
|
||||
if "access okay" in out:
|
||||
return client
|
||||
return None
|
||||
# Paramiko, at this time, seems to return only the ssh-rsa key, so
|
||||
# only the single key is placed into the list.
|
||||
if key:
|
||||
keys.append("%s %s" % (key.get_name(), key.get_base64()))
|
||||
|
||||
return keys
|
||||
|
304
nodepool/provider_manager.py
Normal file → Executable file
304
nodepool/provider_manager.py
Normal file → Executable file
@ -16,39 +16,19 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
|
||||
import shade
|
||||
|
||||
import exceptions
|
||||
import fakeprovider
|
||||
from nodeutils import iterate_timeout
|
||||
from task_manager import TaskManager, ManagerStoppedException
|
||||
from nodepool.driver.fake.provider import FakeProvider
|
||||
from nodepool.driver.openstack.provider import OpenStackProvider
|
||||
|
||||
|
||||
IPS_LIST_AGE = 5 # How long to keep a cached copy of the ip list
|
||||
|
||||
|
||||
@contextmanager
|
||||
def shade_inner_exceptions():
|
||||
try:
|
||||
yield
|
||||
except shade.OpenStackCloudException as e:
|
||||
e.log_error()
|
||||
raise
|
||||
|
||||
|
||||
class NotFound(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_provider_manager(provider, use_taskmanager):
|
||||
if (provider.cloud_config.get_auth_args().get('auth_url') == 'fake'):
|
||||
return FakeProviderManager(provider, use_taskmanager)
|
||||
def get_provider(provider, use_taskmanager):
|
||||
if provider.driver.name == 'fake':
|
||||
return FakeProvider(provider, use_taskmanager)
|
||||
elif provider.driver.name == 'openstack':
|
||||
return OpenStackProvider(provider, use_taskmanager)
|
||||
else:
|
||||
return ProviderManager(provider, use_taskmanager)
|
||||
raise RuntimeError("Unknown provider driver %s" % provider.driver)
|
||||
|
||||
|
||||
class ProviderManager(object):
|
||||
@ -70,7 +50,7 @@ class ProviderManager(object):
|
||||
ProviderManager.log.debug("Creating new ProviderManager object"
|
||||
" for %s" % p.name)
|
||||
new_config.provider_managers[p.name] = \
|
||||
get_provider_manager(p, use_taskmanager)
|
||||
get_provider(p, use_taskmanager)
|
||||
new_config.provider_managers[p.name].start()
|
||||
|
||||
for stop_manager in stop_managers:
|
||||
@ -81,269 +61,3 @@ class ProviderManager(object):
|
||||
for m in config.provider_managers.values():
|
||||
m.stop()
|
||||
m.join()
|
||||
|
||||
def __init__(self, provider, use_taskmanager):
|
||||
self.provider = provider
|
||||
self._images = {}
|
||||
self._networks = {}
|
||||
self.__flavors = {}
|
||||
self._use_taskmanager = use_taskmanager
|
||||
self._taskmanager = None
|
||||
|
||||
def start(self):
|
||||
if self._use_taskmanager:
|
||||
self._taskmanager = TaskManager(None, self.provider.name,
|
||||
self.provider.rate)
|
||||
self._taskmanager.start()
|
||||
self.resetClient()
|
||||
|
||||
def stop(self):
|
||||
if self._taskmanager:
|
||||
self._taskmanager.stop()
|
||||
|
||||
def join(self):
|
||||
if self._taskmanager:
|
||||
self._taskmanager.join()
|
||||
|
||||
@property
|
||||
def _flavors(self):
|
||||
if not self.__flavors:
|
||||
self.__flavors = self._getFlavors()
|
||||
return self.__flavors
|
||||
|
||||
def _getClient(self):
|
||||
if self._use_taskmanager:
|
||||
manager = self._taskmanager
|
||||
else:
|
||||
manager = None
|
||||
return shade.OpenStackCloud(
|
||||
cloud_config=self.provider.cloud_config,
|
||||
manager=manager,
|
||||
**self.provider.cloud_config.config)
|
||||
|
||||
def resetClient(self):
|
||||
self._client = self._getClient()
|
||||
if self._use_taskmanager:
|
||||
self._taskmanager.setClient(self._client)
|
||||
|
||||
def _getFlavors(self):
|
||||
flavors = self.listFlavors()
|
||||
flavors.sort(lambda a, b: cmp(a['ram'], b['ram']))
|
||||
return flavors
|
||||
|
||||
def findFlavor(self, min_ram, name_filter=None):
|
||||
# Note: this will throw an error if the provider is offline
|
||||
# but all the callers are in threads (they call in via CreateServer) so
|
||||
# the mainloop won't be affected.
|
||||
for f in self._flavors:
|
||||
if (f['ram'] >= min_ram
|
||||
and (not name_filter or name_filter in f['name'])):
|
||||
return f
|
||||
raise Exception("Unable to find flavor with min ram: %s" % min_ram)
|
||||
|
||||
def findImage(self, name):
|
||||
if name in self._images:
|
||||
return self._images[name]
|
||||
|
||||
with shade_inner_exceptions():
|
||||
image = self._client.get_image(name)
|
||||
self._images[name] = image
|
||||
return image
|
||||
|
||||
def findNetwork(self, name):
|
||||
if name in self._networks:
|
||||
return self._networks[name]
|
||||
|
||||
with shade_inner_exceptions():
|
||||
network = self._client.get_network(name)
|
||||
self._networks[name] = network
|
||||
return network
|
||||
|
||||
def deleteImage(self, name):
|
||||
if name in self._images:
|
||||
del self._images[name]
|
||||
|
||||
with shade_inner_exceptions():
|
||||
return self._client.delete_image(name)
|
||||
|
||||
def createServer(self, name, min_ram, image_id=None, image_name=None,
|
||||
az=None, key_name=None, name_filter=None,
|
||||
config_drive=True, nodepool_node_id=None,
|
||||
nodepool_image_name=None,
|
||||
nodepool_snapshot_image_id=None):
|
||||
if image_name:
|
||||
image = self.findImage(image_name)
|
||||
else:
|
||||
image = {'id': image_id}
|
||||
flavor = self.findFlavor(min_ram, name_filter)
|
||||
create_args = dict(name=name,
|
||||
image=image,
|
||||
flavor=flavor,
|
||||
config_drive=config_drive)
|
||||
if key_name:
|
||||
create_args['key_name'] = key_name
|
||||
if az:
|
||||
create_args['availability_zone'] = az
|
||||
nics = []
|
||||
for network in self.provider.networks:
|
||||
if network.id:
|
||||
nics.append({'net-id': network.id})
|
||||
elif network.name:
|
||||
net_id = self.findNetwork(network.name)['id']
|
||||
nics.append({'net-id': net_id})
|
||||
else:
|
||||
raise Exception("Invalid 'networks' configuration.")
|
||||
if nics:
|
||||
create_args['nics'] = nics
|
||||
# Put provider.name and image_name in as groups so that ansible
|
||||
# inventory can auto-create groups for us based on each of those
|
||||
# qualities
|
||||
# Also list each of those values directly so that non-ansible
|
||||
# consumption programs don't need to play a game of knowing that
|
||||
# groups[0] is the image name or anything silly like that.
|
||||
nodepool_meta = dict(provider_name=self.provider.name)
|
||||
groups_meta = [self.provider.name]
|
||||
if self.provider.nodepool_id:
|
||||
nodepool_meta['nodepool_id'] = self.provider.nodepool_id
|
||||
if nodepool_node_id:
|
||||
nodepool_meta['node_id'] = nodepool_node_id
|
||||
if nodepool_snapshot_image_id:
|
||||
nodepool_meta['snapshot_image_id'] = nodepool_snapshot_image_id
|
||||
if nodepool_image_name:
|
||||
nodepool_meta['image_name'] = nodepool_image_name
|
||||
groups_meta.append(nodepool_image_name)
|
||||
create_args['meta'] = dict(
|
||||
groups=json.dumps(groups_meta),
|
||||
nodepool=json.dumps(nodepool_meta)
|
||||
)
|
||||
|
||||
with shade_inner_exceptions():
|
||||
return self._client.create_server(wait=False, **create_args)
|
||||
|
||||
def getServer(self, server_id):
|
||||
with shade_inner_exceptions():
|
||||
return self._client.get_server(server_id)
|
||||
|
||||
def waitForServer(self, server, timeout=3600):
|
||||
with shade_inner_exceptions():
|
||||
return self._client.wait_for_server(
|
||||
server=server, auto_ip=True, reuse=False,
|
||||
timeout=timeout)
|
||||
|
||||
def waitForServerDeletion(self, server_id, timeout=600):
|
||||
for count in iterate_timeout(
|
||||
timeout, exceptions.ServerDeleteException,
|
||||
"server %s deletion" % server_id):
|
||||
if not self.getServer(server_id):
|
||||
return
|
||||
|
||||
def waitForImage(self, image_id, timeout=3600):
|
||||
last_status = None
|
||||
for count in iterate_timeout(
|
||||
timeout, exceptions.ImageCreateException, "image creation"):
|
||||
try:
|
||||
image = self.getImage(image_id)
|
||||
except NotFound:
|
||||
continue
|
||||
except ManagerStoppedException:
|
||||
raise
|
||||
except Exception:
|
||||
self.log.exception('Unable to list images while waiting for '
|
||||
'%s will retry' % (image_id))
|
||||
continue
|
||||
|
||||
# shade returns None when not found
|
||||
if not image:
|
||||
continue
|
||||
|
||||
status = image['status']
|
||||
if (last_status != status):
|
||||
self.log.debug(
|
||||
'Status of image in {provider} {id}: {status}'.format(
|
||||
provider=self.provider.name,
|
||||
id=image_id,
|
||||
status=status))
|
||||
if status == 'ERROR' and 'fault' in image:
|
||||
self.log.debug(
|
||||
'ERROR in {provider} on {id}: {resason}'.format(
|
||||
provider=self.provider.name,
|
||||
id=image_id,
|
||||
resason=image['fault']['message']))
|
||||
last_status = status
|
||||
# Glance client returns lower case statuses - but let's be sure
|
||||
if status.lower() in ['active', 'error']:
|
||||
return image
|
||||
|
||||
def createImage(self, server, image_name, meta):
|
||||
with shade_inner_exceptions():
|
||||
return self._client.create_image_snapshot(
|
||||
image_name, server, **meta)
|
||||
|
||||
def getImage(self, image_id):
|
||||
with shade_inner_exceptions():
|
||||
return self._client.get_image(image_id)
|
||||
|
||||
def uploadImage(self, image_name, filename, image_type=None, meta=None,
|
||||
md5=None, sha256=None):
|
||||
# configure glance and upload image. Note the meta flags
|
||||
# are provided as custom glance properties
|
||||
# NOTE: we have wait=True set here. This is not how we normally
|
||||
# do things in nodepool, preferring to poll ourselves thankyouverymuch.
|
||||
# However - two things to note:
|
||||
# - PUT has no aysnc mechanism, so we have to handle it anyway
|
||||
# - v2 w/task waiting is very strange and complex - but we have to
|
||||
# block for our v1 clouds anyway, so we might as well
|
||||
# have the interface be the same and treat faking-out
|
||||
# a shade-level fake-async interface later
|
||||
if not meta:
|
||||
meta = {}
|
||||
if image_type:
|
||||
meta['disk_format'] = image_type
|
||||
with shade_inner_exceptions():
|
||||
image = self._client.create_image(
|
||||
name=image_name,
|
||||
filename=filename,
|
||||
is_public=False,
|
||||
wait=True,
|
||||
md5=md5,
|
||||
sha256=sha256,
|
||||
**meta)
|
||||
return image.id
|
||||
|
||||
def listImages(self):
|
||||
with shade_inner_exceptions():
|
||||
return self._client.list_images()
|
||||
|
||||
def listFlavors(self):
|
||||
with shade_inner_exceptions():
|
||||
return self._client.list_flavors(get_extra=False)
|
||||
|
||||
def listServers(self):
|
||||
# shade list_servers carries the nodepool server list caching logic
|
||||
with shade_inner_exceptions():
|
||||
return self._client.list_servers()
|
||||
|
||||
def deleteServer(self, server_id):
|
||||
with shade_inner_exceptions():
|
||||
return self._client.delete_server(server_id, delete_ips=True)
|
||||
|
||||
def cleanupServer(self, server_id):
|
||||
server = self.getServer(server_id)
|
||||
if not server:
|
||||
raise NotFound()
|
||||
|
||||
self.log.debug('Deleting server %s' % server_id)
|
||||
self.deleteServer(server_id)
|
||||
|
||||
def cleanupLeakedFloaters(self):
|
||||
with shade_inner_exceptions():
|
||||
self._client.delete_unattached_floating_ips()
|
||||
|
||||
|
||||
class FakeProviderManager(ProviderManager):
|
||||
def __init__(self, provider, use_taskmanager):
|
||||
self.__client = fakeprovider.FakeOpenStackCloud()
|
||||
super(FakeProviderManager, self).__init__(provider, use_taskmanager)
|
||||
|
||||
def _getClient(self):
|
||||
return self.__client
|
||||
|
@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Update the base image that is used for devstack VMs.
|
||||
|
||||
# Copyright (C) 2011-2012 OpenStack LLC.
|
||||
#
|
||||
# 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 paramiko
|
||||
|
||||
|
||||
class SSHClient(object):
|
||||
def __init__(self, ip, username, password=None, pkey=None,
|
||||
key_filename=None, log=None, look_for_keys=False,
|
||||
allow_agent=False):
|
||||
self.client = paramiko.SSHClient()
|
||||
self.client.set_missing_host_key_policy(paramiko.WarningPolicy())
|
||||
self.client.connect(ip, username=username, password=password,
|
||||
pkey=pkey, key_filename=key_filename,
|
||||
look_for_keys=look_for_keys,
|
||||
allow_agent=allow_agent)
|
||||
self.log = log
|
||||
|
||||
def __del__(self):
|
||||
self.client.close()
|
||||
|
||||
def ssh(self, action, command, get_pty=True, output=False):
|
||||
if self.log:
|
||||
self.log.debug("*** START to %s" % action)
|
||||
self.log.debug("executing: %s" % command)
|
||||
stdin, stdout, stderr = self.client.exec_command(
|
||||
command, get_pty=get_pty)
|
||||
out = ''
|
||||
err = ''
|
||||
for line in stdout:
|
||||
if output:
|
||||
out += line
|
||||
if self.log:
|
||||
self.log.info(line.rstrip())
|
||||
for line in stderr:
|
||||
if output:
|
||||
err += line
|
||||
if self.log:
|
||||
self.log.error(line.rstrip())
|
||||
ret = stdout.channel.recv_exit_status()
|
||||
if ret:
|
||||
if self.log:
|
||||
self.log.debug("*** FAILED to %s (%s)" % (action, ret))
|
||||
raise Exception(
|
||||
"Unable to %s\ncommand: %s\nstdout: %s\nstderr: %s"
|
||||
% (action, command, out, err))
|
||||
if self.log:
|
||||
self.log.debug("*** SUCCESSFULLY %s" % action)
|
||||
return out
|
||||
|
||||
def scp(self, source, dest):
|
||||
if self.log:
|
||||
self.log.info("Copy %s -> %s" % (source, dest))
|
||||
ftp = self.client.open_sftp()
|
||||
ftp.put(source, dest)
|
||||
ftp.close()
|
98
nodepool/stats.py
Normal file → Executable file
98
nodepool/stats.py
Normal file → Executable file
@ -20,8 +20,11 @@ import os
|
||||
import logging
|
||||
import statsd
|
||||
|
||||
from nodepool import zk
|
||||
|
||||
log = logging.getLogger("nodepool.stats")
|
||||
|
||||
|
||||
def get_client():
|
||||
"""Return a statsd client object setup from environment variables; or
|
||||
None if they are not set
|
||||
@ -38,3 +41,98 @@ def get_client():
|
||||
return statsd.StatsClient(**statsd_args)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class StatsReporter(object):
|
||||
'''
|
||||
Class adding statsd reporting functionality.
|
||||
'''
|
||||
def __init__(self):
|
||||
super(StatsReporter, self).__init__()
|
||||
self._statsd = get_client()
|
||||
|
||||
def recordLaunchStats(self, subkey, dt, image_name,
|
||||
provider_name, node_az, requestor):
|
||||
'''
|
||||
Record node launch statistics.
|
||||
|
||||
:param str subkey: statsd key
|
||||
:param int dt: Time delta in milliseconds
|
||||
:param str image_name: Name of the image used
|
||||
:param str provider_name: Name of the provider
|
||||
:param str node_az: AZ of the launched node
|
||||
:param str requestor: Identifier for the request originator
|
||||
'''
|
||||
if not self._statsd:
|
||||
return
|
||||
|
||||
keys = [
|
||||
'nodepool.launch.provider.%s.%s' % (provider_name, subkey),
|
||||
'nodepool.launch.image.%s.%s' % (image_name, subkey),
|
||||
'nodepool.launch.%s' % (subkey,),
|
||||
]
|
||||
|
||||
if node_az:
|
||||
keys.append('nodepool.launch.provider.%s.%s.%s' %
|
||||
(provider_name, node_az, subkey))
|
||||
|
||||
if requestor:
|
||||
# Replace '.' which is a graphite hierarchy, and ':' which is
|
||||
# a statsd delimeter.
|
||||
requestor = requestor.replace('.', '_')
|
||||
requestor = requestor.replace(':', '_')
|
||||
keys.append('nodepool.launch.requestor.%s.%s' %
|
||||
(requestor, subkey))
|
||||
|
||||
for key in keys:
|
||||
self._statsd.timing(key, dt)
|
||||
self._statsd.incr(key)
|
||||
|
||||
def updateNodeStats(self, zk_conn, provider):
|
||||
'''
|
||||
Refresh statistics for all known nodes.
|
||||
|
||||
:param ZooKeeper zk_conn: A ZooKeeper connection object.
|
||||
:param Provider provider: A config Provider object.
|
||||
'''
|
||||
if not self._statsd:
|
||||
return
|
||||
|
||||
states = {}
|
||||
|
||||
# Initialize things we know about to zero
|
||||
for state in zk.Node.VALID_STATES:
|
||||
key = 'nodepool.nodes.%s' % state
|
||||
states[key] = 0
|
||||
key = 'nodepool.provider.%s.nodes.%s' % (provider.name, state)
|
||||
states[key] = 0
|
||||
|
||||
for node in zk_conn.nodeIterator():
|
||||
# nodepool.nodes.STATE
|
||||
key = 'nodepool.nodes.%s' % node.state
|
||||
states[key] += 1
|
||||
|
||||
# nodepool.label.LABEL.nodes.STATE
|
||||
key = 'nodepool.label.%s.nodes.%s' % (node.type, node.state)
|
||||
# It's possible we could see node types that aren't in our config
|
||||
if key in states:
|
||||
states[key] += 1
|
||||
else:
|
||||
states[key] = 1
|
||||
|
||||
# nodepool.provider.PROVIDER.nodes.STATE
|
||||
key = 'nodepool.provider.%s.nodes.%s' % (node.provider, node.state)
|
||||
# It's possible we could see providers that aren't in our config
|
||||
if key in states:
|
||||
states[key] += 1
|
||||
else:
|
||||
states[key] = 1
|
||||
|
||||
for key, count in states.items():
|
||||
self._statsd.gauge(key, count)
|
||||
|
||||
# nodepool.provider.PROVIDER.max_servers
|
||||
key = 'nodepool.provider.%s.max_servers' % provider.name
|
||||
max_servers = sum([p.max_servers for p in provider.pools.values()
|
||||
if p.max_servers])
|
||||
self._statsd.gauge(key, max_servers)
|
||||
|
127
nodepool/status.py
Normal file → Executable file
127
nodepool/status.py
Normal file → Executable file
@ -17,8 +17,6 @@
|
||||
import json
|
||||
import time
|
||||
|
||||
from nodepool import nodedb
|
||||
|
||||
from prettytable import PrettyTable
|
||||
|
||||
|
||||
@ -31,21 +29,101 @@ def age(timestamp):
|
||||
return '%02d:%02d:%02d:%02d' % (d, h, m, s)
|
||||
|
||||
|
||||
def node_list(db, node_id=None):
|
||||
t = PrettyTable(["ID", "Provider", "AZ", "Label", "Target",
|
||||
"Manager", "Hostname", "NodeName", "Server ID",
|
||||
"IP", "State", "Age", "Comment"])
|
||||
def node_list(zk, node_id=None, detail=False):
|
||||
headers = [
|
||||
"ID",
|
||||
"Provider",
|
||||
"Label",
|
||||
"Server ID",
|
||||
"Public IPv4",
|
||||
"IPv6",
|
||||
"State",
|
||||
"Age",
|
||||
"Locked"
|
||||
]
|
||||
detail_headers = [
|
||||
"Hostname",
|
||||
"Private IPv4",
|
||||
"AZ",
|
||||
"Port",
|
||||
"Launcher",
|
||||
"Allocated To",
|
||||
"Hold Job",
|
||||
"Comment"
|
||||
]
|
||||
if detail:
|
||||
headers += detail_headers
|
||||
|
||||
t = PrettyTable(headers)
|
||||
t.align = 'l'
|
||||
with db.getSession() as session:
|
||||
for node in session.getNodes():
|
||||
if node_id and node.id != node_id:
|
||||
continue
|
||||
t.add_row([node.id, node.provider_name, node.az,
|
||||
node.label_name, node.target_name,
|
||||
node.manager_name, node.hostname,
|
||||
node.nodename, node.external_id, node.ip,
|
||||
nodedb.STATE_NAMES[node.state],
|
||||
age(node.state_time), node.comment])
|
||||
|
||||
if node_id:
|
||||
node = zk.getNode(node_id)
|
||||
if node:
|
||||
locked = "unlocked"
|
||||
try:
|
||||
zk.lockNode(node, blocking=False)
|
||||
except Exception:
|
||||
locked = "locked"
|
||||
else:
|
||||
zk.unlockNode(node)
|
||||
|
||||
values = [
|
||||
node.id,
|
||||
node.provider,
|
||||
node.type,
|
||||
node.external_id,
|
||||
node.public_ipv4,
|
||||
node.public_ipv6,
|
||||
node.state,
|
||||
age(node.state_time),
|
||||
locked
|
||||
]
|
||||
if detail:
|
||||
values += [
|
||||
node.hostname,
|
||||
node.private_ipv4,
|
||||
node.az,
|
||||
node.connection_port,
|
||||
node.launcher,
|
||||
node.allocated_to,
|
||||
node.hold_job,
|
||||
node.comment
|
||||
]
|
||||
t.add_row(values)
|
||||
else:
|
||||
for node in zk.nodeIterator():
|
||||
locked = "unlocked"
|
||||
try:
|
||||
zk.lockNode(node, blocking=False)
|
||||
except Exception:
|
||||
locked = "locked"
|
||||
else:
|
||||
zk.unlockNode(node)
|
||||
|
||||
values = [
|
||||
node.id,
|
||||
node.provider,
|
||||
node.type,
|
||||
node.external_id,
|
||||
node.public_ipv4,
|
||||
node.public_ipv6,
|
||||
node.state,
|
||||
age(node.state_time),
|
||||
locked
|
||||
]
|
||||
if detail:
|
||||
values += [
|
||||
node.hostname,
|
||||
node.private_ipv4,
|
||||
node.az,
|
||||
node.connection_port,
|
||||
node.launcher,
|
||||
node.allocated_to,
|
||||
node.hold_job,
|
||||
node.comment
|
||||
]
|
||||
t.add_row(values)
|
||||
return str(t)
|
||||
|
||||
|
||||
@ -67,15 +145,16 @@ def dib_image_list_json(zk):
|
||||
for image_name in zk.getImageNames():
|
||||
for build_no in zk.getBuildNumbers(image_name):
|
||||
build = zk.getBuild(image_name, build_no)
|
||||
objs.append({'id' : '-'.join([image_name, build_no]),
|
||||
objs.append({'id': '-'.join([image_name, build_no]),
|
||||
'image': image_name,
|
||||
'builder': build.builder,
|
||||
'formats': build.formats,
|
||||
'state': build.state,
|
||||
'age': int(build.state_time)
|
||||
})
|
||||
})
|
||||
return json.dumps(objs)
|
||||
|
||||
|
||||
def image_list(zk):
|
||||
t = PrettyTable(["Build ID", "Upload ID", "Provider", "Image",
|
||||
"Provider Image Name", "Provider Image ID", "State",
|
||||
@ -94,3 +173,15 @@ def image_list(zk):
|
||||
upload.state,
|
||||
age(upload.state_time)])
|
||||
return str(t)
|
||||
|
||||
|
||||
def request_list(zk):
|
||||
t = PrettyTable(["Request ID", "State", "Requestor", "Node Types", "Nodes",
|
||||
"Declined By"])
|
||||
t.align = 'l'
|
||||
for req in zk.nodeRequestIterator():
|
||||
t.add_row([req.id, req.state, req.requestor,
|
||||
','.join(req.node_types),
|
||||
','.join(req.nodes),
|
||||
','.join(req.declined_by)])
|
||||
return str(t)
|
||||
|
@ -18,12 +18,14 @@
|
||||
|
||||
import sys
|
||||
import threading
|
||||
import six
|
||||
from six.moves import queue as Queue
|
||||
import logging
|
||||
import time
|
||||
import requests.exceptions
|
||||
|
||||
import stats
|
||||
from nodepool import stats
|
||||
|
||||
|
||||
class ManagerStoppedException(Exception):
|
||||
pass
|
||||
@ -49,7 +51,7 @@ class Task(object):
|
||||
def wait(self):
|
||||
self._wait_event.wait()
|
||||
if self._exception:
|
||||
raise self._exception, None, self._traceback
|
||||
six.reraise(self._exception, None, self._traceback)
|
||||
return self._result
|
||||
|
||||
def run(self, client):
|
||||
@ -105,7 +107,7 @@ class TaskManager(threading.Thread):
|
||||
self.log.debug("Manager %s ran task %s in %ss" %
|
||||
(self.name, type(task).__name__, dt))
|
||||
if self.statsd:
|
||||
#nodepool.task.PROVIDER.subkey
|
||||
# nodepool.task.PROVIDER.subkey
|
||||
subkey = type(task).__name__
|
||||
key = 'nodepool.task.%s.%s' % (self.name, subkey)
|
||||
self.statsd.timing(key, int(dt * 1000))
|
||||
|
@ -15,27 +15,25 @@
|
||||
|
||||
"""Common utilities used in testing"""
|
||||
|
||||
import errno
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import pymysql
|
||||
import random
|
||||
import re
|
||||
import select
|
||||
import string
|
||||
import socket
|
||||
import subprocess
|
||||
import threading
|
||||
import tempfile
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import fixtures
|
||||
import gear
|
||||
import lockfile
|
||||
import kazoo.client
|
||||
import testtools
|
||||
|
||||
from nodepool import allocation, builder, fakeprovider, nodepool, nodedb, webapp
|
||||
from nodepool import builder
|
||||
from nodepool import launcher
|
||||
from nodepool import webapp
|
||||
from nodepool import zk
|
||||
from nodepool.cmd.config_validator import ConfigValidator
|
||||
|
||||
@ -46,74 +44,6 @@ class LoggingPopen(subprocess.Popen):
|
||||
pass
|
||||
|
||||
|
||||
class FakeGearmanServer(gear.Server):
|
||||
def __init__(self, port=0):
|
||||
self.hold_jobs_in_queue = False
|
||||
super(FakeGearmanServer, self).__init__(port)
|
||||
|
||||
def getJobForConnection(self, connection, peek=False):
|
||||
for queue in [self.high_queue, self.normal_queue, self.low_queue]:
|
||||
for job in queue:
|
||||
if not hasattr(job, 'waiting'):
|
||||
if job.name.startswith('build:'):
|
||||
job.waiting = self.hold_jobs_in_queue
|
||||
else:
|
||||
job.waiting = False
|
||||
if job.waiting:
|
||||
continue
|
||||
if job.name in connection.functions:
|
||||
if not peek:
|
||||
queue.remove(job)
|
||||
connection.related_jobs[job.handle] = job
|
||||
job.worker_connection = connection
|
||||
job.running = True
|
||||
return job
|
||||
return None
|
||||
|
||||
def release(self, regex=None):
|
||||
released = False
|
||||
qlen = (len(self.high_queue) + len(self.normal_queue) +
|
||||
len(self.low_queue))
|
||||
self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
|
||||
for job in self.getQueue():
|
||||
cmd, name = job.name.split(':')
|
||||
if cmd != 'build':
|
||||
continue
|
||||
if not regex or re.match(regex, name):
|
||||
self.log.debug("releasing queued job %s" %
|
||||
job.unique)
|
||||
job.waiting = False
|
||||
released = True
|
||||
else:
|
||||
self.log.debug("not releasing queued job %s" %
|
||||
job.unique)
|
||||
if released:
|
||||
self.wakeConnections()
|
||||
qlen = (len(self.high_queue) + len(self.normal_queue) +
|
||||
len(self.low_queue))
|
||||
self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
|
||||
|
||||
|
||||
class GearmanServerFixture(fixtures.Fixture):
|
||||
def __init__(self, port=0):
|
||||
self._port = port
|
||||
|
||||
def setUp(self):
|
||||
super(GearmanServerFixture, self).setUp()
|
||||
self.gearman_server = FakeGearmanServer(self._port)
|
||||
self.addCleanup(self.shutdownGearman)
|
||||
|
||||
def shutdownGearman(self):
|
||||
#TODO:greghaynes remove try once gear client protects against this
|
||||
try:
|
||||
self.gearman_server.shutdown()
|
||||
except OSError as e:
|
||||
if e.errno == errno.EBADF:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
class ZookeeperServerFixture(fixtures.Fixture):
|
||||
def _setUp(self):
|
||||
zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
|
||||
@ -171,35 +101,38 @@ class ChrootedKazooFixture(fixtures.Fixture):
|
||||
_tmp_client.close()
|
||||
|
||||
|
||||
class GearmanClient(gear.Client):
|
||||
def __init__(self):
|
||||
super(GearmanClient, self).__init__(client_id='test_client')
|
||||
self.__log = logging.getLogger("tests.GearmanClient")
|
||||
class StatsdFixture(fixtures.Fixture):
|
||||
def _setUp(self):
|
||||
self.running = True
|
||||
self.thread = threading.Thread(target=self.run)
|
||||
self.thread.daemon = True
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.sock.bind(('', 0))
|
||||
self.port = self.sock.getsockname()[1]
|
||||
self.wake_read, self.wake_write = os.pipe()
|
||||
self.stats = []
|
||||
self.thread.start()
|
||||
self.addCleanup(self._cleanup)
|
||||
|
||||
def get_queued_image_jobs(self):
|
||||
'Count the number of image-build and upload jobs queued.'
|
||||
queued = 0
|
||||
for connection in self.active_connections:
|
||||
try:
|
||||
req = gear.StatusAdminRequest()
|
||||
connection.sendAdminRequest(req)
|
||||
except Exception:
|
||||
self.__log.exception("Exception while listing functions")
|
||||
self._lostConnection(connection)
|
||||
continue
|
||||
for line in req.response.split('\n'):
|
||||
parts = [x.strip() for x in line.split('\t')]
|
||||
# parts[0] - function name
|
||||
# parts[1] - total jobs queued (including building)
|
||||
# parts[2] - jobs building
|
||||
# parts[3] - workers registered
|
||||
if not parts or parts[0] == '.':
|
||||
continue
|
||||
if (not parts[0].startswith('image-build:') and
|
||||
not parts[0].startswith('image-upload:')):
|
||||
continue
|
||||
queued += int(parts[1])
|
||||
return queued
|
||||
def run(self):
|
||||
while self.running:
|
||||
poll = select.poll()
|
||||
poll.register(self.sock, select.POLLIN)
|
||||
poll.register(self.wake_read, select.POLLIN)
|
||||
ret = poll.poll()
|
||||
for (fd, event) in ret:
|
||||
if fd == self.sock.fileno():
|
||||
data = self.sock.recvfrom(1024)
|
||||
if not data:
|
||||
return
|
||||
self.stats.append(data[0])
|
||||
if fd == self.wake_read:
|
||||
return
|
||||
|
||||
def _cleanup(self):
|
||||
self.running = False
|
||||
os.write(self.wake_write, b'1\n')
|
||||
self.thread.join()
|
||||
|
||||
|
||||
class BaseTestCase(testtools.TestCase):
|
||||
@ -230,7 +163,10 @@ class BaseTestCase(testtools.TestCase):
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
l = logging.getLogger('kazoo')
|
||||
l.setLevel(logging.INFO)
|
||||
l.propagate=False
|
||||
l.propagate = False
|
||||
l = logging.getLogger('stevedore')
|
||||
l.setLevel(logging.INFO)
|
||||
l.propagate = False
|
||||
self.useFixture(fixtures.NestedTempfile())
|
||||
|
||||
self.subprocesses = []
|
||||
@ -240,48 +176,46 @@ class BaseTestCase(testtools.TestCase):
|
||||
self.subprocesses.append(p)
|
||||
return p
|
||||
|
||||
self.statsd = StatsdFixture()
|
||||
self.useFixture(self.statsd)
|
||||
|
||||
# note, use 127.0.0.1 rather than localhost to avoid getting ipv6
|
||||
# see: https://github.com/jsocol/pystatsd/issues/61
|
||||
os.environ['STATSD_HOST'] = '127.0.0.1'
|
||||
os.environ['STATSD_PORT'] = str(self.statsd.port)
|
||||
|
||||
self.useFixture(fixtures.MonkeyPatch('subprocess.Popen',
|
||||
LoggingPopenFactory))
|
||||
self.setUpFakes()
|
||||
|
||||
def setUpFakes(self):
|
||||
log = logging.getLogger("nodepool.test")
|
||||
log.debug("set up fakes")
|
||||
fake_client = fakeprovider.FakeOpenStackCloud()
|
||||
|
||||
def get_fake_client(*args, **kwargs):
|
||||
return fake_client
|
||||
|
||||
clouds_path = os.path.join(os.path.dirname(__file__),
|
||||
'fixtures', 'clouds.yaml')
|
||||
self.useFixture(fixtures.MonkeyPatch(
|
||||
'nodepool.provider_manager.ProviderManager._getClient',
|
||||
get_fake_client))
|
||||
self.useFixture(fixtures.MonkeyPatch(
|
||||
'nodepool.nodepool._get_one_cloud',
|
||||
fakeprovider.fake_get_one_cloud))
|
||||
'os_client_config.config.CONFIG_FILES', [clouds_path]))
|
||||
|
||||
def wait_for_threads(self):
|
||||
whitelist = ['APScheduler',
|
||||
'MainThread',
|
||||
# Wait until all transient threads (node launches, deletions,
|
||||
# etc.) are all complete. Whitelist any long-running threads.
|
||||
whitelist = ['MainThread',
|
||||
'NodePool',
|
||||
'NodePool Builder',
|
||||
'NodeUpdateListener',
|
||||
'Gearman client connect',
|
||||
'Gearman client poll',
|
||||
'fake-provider',
|
||||
'fake-provider1',
|
||||
'fake-provider2',
|
||||
'fake-provider3',
|
||||
'fake-dib-provider',
|
||||
'fake-jenkins',
|
||||
'fake-target',
|
||||
'DiskImageBuilder queue',
|
||||
'CleanupWorker',
|
||||
'DeletedNodeWorker',
|
||||
'pydevd.CommandThread',
|
||||
'pydevd.Reader',
|
||||
'pydevd.Writer',
|
||||
]
|
||||
|
||||
while True:
|
||||
done = True
|
||||
for t in threading.enumerate():
|
||||
if t.name.startswith("Thread-"):
|
||||
# apscheduler thread pool
|
||||
# Kazoo
|
||||
continue
|
||||
if t.name.startswith("worker "):
|
||||
# paste web server
|
||||
@ -292,93 +226,45 @@ class BaseTestCase(testtools.TestCase):
|
||||
continue
|
||||
if t.name.startswith("CleanupWorker"):
|
||||
continue
|
||||
if t.name.startswith("PoolWorker"):
|
||||
continue
|
||||
if t.name not in whitelist:
|
||||
done = False
|
||||
if done:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
|
||||
def assertReportedStat(self, key, value=None, kind=None):
|
||||
start = time.time()
|
||||
while time.time() < (start + 5):
|
||||
for stat in self.statsd.stats:
|
||||
k, v = stat.decode('utf8').split(':')
|
||||
if key == k:
|
||||
if value is None and kind is None:
|
||||
return
|
||||
elif value:
|
||||
if value == v:
|
||||
return
|
||||
elif kind:
|
||||
if v.endswith('|' + kind):
|
||||
return
|
||||
time.sleep(0.1)
|
||||
|
||||
class AllocatorTestCase(object):
|
||||
def setUp(self):
|
||||
super(AllocatorTestCase, self).setUp()
|
||||
self.agt = []
|
||||
|
||||
def test_allocator(self):
|
||||
for i, amount in enumerate(self.results):
|
||||
print self.agt[i]
|
||||
for i, amount in enumerate(self.results):
|
||||
self.assertEqual(self.agt[i].amount, amount,
|
||||
'Error at pos %d, '
|
||||
'expected %s and got %s' % (i, self.results,
|
||||
[x.amount
|
||||
for x in self.agt]))
|
||||
|
||||
|
||||
class RoundRobinTestCase(object):
|
||||
def setUp(self):
|
||||
super(RoundRobinTestCase, self).setUp()
|
||||
self.allocations = []
|
||||
|
||||
def test_allocator(self):
|
||||
for i, label in enumerate(self.results):
|
||||
self.assertEqual(self.results[i], self.allocations[i],
|
||||
'Error at pos %d, '
|
||||
'expected %s and got %s' % (i, self.results,
|
||||
self.allocations))
|
||||
|
||||
|
||||
class MySQLSchemaFixture(fixtures.Fixture):
|
||||
def setUp(self):
|
||||
super(MySQLSchemaFixture, self).setUp()
|
||||
|
||||
random_bits = ''.join(random.choice(string.ascii_lowercase +
|
||||
string.ascii_uppercase)
|
||||
for x in range(8))
|
||||
self.name = '%s_%s' % (random_bits, os.getpid())
|
||||
self.passwd = uuid.uuid4().hex
|
||||
lock = lockfile.LockFile('/tmp/nodepool-db-schema-lockfile')
|
||||
with lock:
|
||||
db = pymysql.connect(host="localhost",
|
||||
user="openstack_citest",
|
||||
passwd="openstack_citest",
|
||||
db="openstack_citest")
|
||||
cur = db.cursor()
|
||||
cur.execute("create database %s" % self.name)
|
||||
cur.execute(
|
||||
"grant all on %s.* to '%s'@'localhost' identified by '%s'" %
|
||||
(self.name, self.name, self.passwd))
|
||||
cur.execute("flush privileges")
|
||||
|
||||
self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
|
||||
self.passwd,
|
||||
self.name)
|
||||
self.addDetail('dburi', testtools.content.text_content(self.dburi))
|
||||
self.addCleanup(self.cleanup)
|
||||
|
||||
def cleanup(self):
|
||||
lock = lockfile.LockFile('/tmp/nodepool-db-schema-lockfile')
|
||||
with lock:
|
||||
db = pymysql.connect(host="localhost",
|
||||
user="openstack_citest",
|
||||
passwd="openstack_citest",
|
||||
db="openstack_citest")
|
||||
cur = db.cursor()
|
||||
cur.execute("drop database %s" % self.name)
|
||||
cur.execute("drop user '%s'@'localhost'" % self.name)
|
||||
cur.execute("flush privileges")
|
||||
raise Exception("Key %s not found in reported stats" % key)
|
||||
|
||||
|
||||
class BuilderFixture(fixtures.Fixture):
|
||||
def __init__(self, configfile, cleanup_interval):
|
||||
def __init__(self, configfile, cleanup_interval, securefile=None):
|
||||
super(BuilderFixture, self).__init__()
|
||||
self.configfile = configfile
|
||||
self.securefile = securefile
|
||||
self.cleanup_interval = cleanup_interval
|
||||
self.builder = None
|
||||
|
||||
def setUp(self):
|
||||
super(BuilderFixture, self).setUp()
|
||||
self.builder = builder.NodePoolBuilder(self.configfile)
|
||||
self.builder = builder.NodePoolBuilder(
|
||||
self.configfile, secure_path=self.securefile)
|
||||
self.builder.cleanup_interval = self.cleanup_interval
|
||||
self.builder.build_interval = .1
|
||||
self.builder.upload_interval = .1
|
||||
@ -394,15 +280,6 @@ class DBTestCase(BaseTestCase):
|
||||
def setUp(self):
|
||||
super(DBTestCase, self).setUp()
|
||||
self.log = logging.getLogger("tests")
|
||||
f = MySQLSchemaFixture()
|
||||
self.useFixture(f)
|
||||
self.dburi = f.dburi
|
||||
self.secure_conf = self._setup_secure()
|
||||
|
||||
gearman_fixture = GearmanServerFixture()
|
||||
self.useFixture(gearman_fixture)
|
||||
self.gearman_server = gearman_fixture.gearman_server
|
||||
|
||||
self.setupZK()
|
||||
|
||||
def setup_config(self, filename, images_dir=None):
|
||||
@ -412,13 +289,13 @@ class DBTestCase(BaseTestCase):
|
||||
configfile = os.path.join(os.path.dirname(__file__),
|
||||
'fixtures', filename)
|
||||
(fd, path) = tempfile.mkstemp()
|
||||
with open(configfile) as conf_fd:
|
||||
config = conf_fd.read()
|
||||
os.write(fd, config.format(images_dir=images_dir.path,
|
||||
gearman_port=self.gearman_server.port,
|
||||
zookeeper_host=self.zookeeper_host,
|
||||
zookeeper_port=self.zookeeper_port,
|
||||
zookeeper_chroot=self.zookeeper_chroot))
|
||||
with open(configfile, 'rb') as conf_fd:
|
||||
config = conf_fd.read().decode('utf8')
|
||||
data = config.format(images_dir=images_dir.path,
|
||||
zookeeper_host=self.zookeeper_host,
|
||||
zookeeper_port=self.zookeeper_port,
|
||||
zookeeper_chroot=self.zookeeper_chroot)
|
||||
os.write(fd, data.encode('utf8'))
|
||||
os.close(fd)
|
||||
self._config_images_dir = images_dir
|
||||
validator = ConfigValidator(path)
|
||||
@ -430,14 +307,18 @@ class DBTestCase(BaseTestCase):
|
||||
new_configfile = self.setup_config(filename, self._config_images_dir)
|
||||
os.rename(new_configfile, configfile)
|
||||
|
||||
def _setup_secure(self):
|
||||
def setup_secure(self, filename):
|
||||
# replace entries in secure.conf
|
||||
configfile = os.path.join(os.path.dirname(__file__),
|
||||
'fixtures', 'secure.conf')
|
||||
'fixtures', filename)
|
||||
(fd, path) = tempfile.mkstemp()
|
||||
with open(configfile) as conf_fd:
|
||||
config = conf_fd.read()
|
||||
os.write(fd, config.format(dburi=self.dburi))
|
||||
with open(configfile, 'rb') as conf_fd:
|
||||
config = conf_fd.read().decode('utf8')
|
||||
data = config.format(
|
||||
zookeeper_host=self.zookeeper_host,
|
||||
zookeeper_port=self.zookeeper_port,
|
||||
zookeeper_chroot=self.zookeeper_chroot)
|
||||
os.write(fd, data.encode('utf8'))
|
||||
os.close(fd)
|
||||
return path
|
||||
|
||||
@ -527,35 +408,65 @@ class DBTestCase(BaseTestCase):
|
||||
|
||||
self.wait_for_threads()
|
||||
|
||||
def waitForNodes(self, pool):
|
||||
self.wait_for_config(pool)
|
||||
allocation_history = allocation.AllocationHistory()
|
||||
def waitForNodeDeletion(self, node):
|
||||
while True:
|
||||
exists = False
|
||||
for n in self.zk.nodeIterator():
|
||||
if node.id == n.id:
|
||||
exists = True
|
||||
break
|
||||
if not exists:
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
def waitForInstanceDeletion(self, manager, instance_id):
|
||||
while True:
|
||||
servers = manager.listNodes()
|
||||
if not (instance_id in [s.id for s in servers]):
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
def waitForNodeRequestLockDeletion(self, request_id):
|
||||
while True:
|
||||
exists = False
|
||||
for lock_id in self.zk.getNodeRequestLockIDs():
|
||||
if request_id == lock_id:
|
||||
exists = True
|
||||
break
|
||||
if not exists:
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
def waitForNodes(self, label, count=1):
|
||||
while True:
|
||||
self.wait_for_threads()
|
||||
with pool.getDB().getSession() as session:
|
||||
needed = pool.getNeededNodes(session, allocation_history)
|
||||
if not needed:
|
||||
nodes = session.getNodes(state=nodedb.BUILDING)
|
||||
if not nodes:
|
||||
break
|
||||
ready_nodes = self.zk.getReadyNodesOfTypes([label])
|
||||
if label in ready_nodes and len(ready_nodes[label]) == count:
|
||||
break
|
||||
time.sleep(1)
|
||||
self.wait_for_threads()
|
||||
return ready_nodes[label]
|
||||
|
||||
def waitForJobs(self):
|
||||
# XXX:greghaynes - There is a very narrow race here where nodepool
|
||||
# is who actually updates the database so this may return before the
|
||||
# image rows are updated.
|
||||
client = GearmanClient()
|
||||
client.addServer('localhost', self.gearman_server.port)
|
||||
client.waitForServer()
|
||||
def waitForNodeRequest(self, req, states=None):
|
||||
'''
|
||||
Wait for a node request to transition to a final state.
|
||||
'''
|
||||
if states is None:
|
||||
states = (zk.FULFILLED, zk.FAILED)
|
||||
while True:
|
||||
req = self.zk.getNodeRequest(req.id)
|
||||
if req.state in states:
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
while client.get_queued_image_jobs() > 0:
|
||||
time.sleep(.2)
|
||||
client.shutdown()
|
||||
return req
|
||||
|
||||
def useNodepool(self, *args, **kwargs):
|
||||
args = (self.secure_conf,) + args
|
||||
pool = nodepool.NodePool(*args, **kwargs)
|
||||
secure_conf = kwargs.pop('secure_conf', None)
|
||||
args = (secure_conf,) + args
|
||||
pool = launcher.NodePool(*args, **kwargs)
|
||||
pool.cleanup_interval = .5
|
||||
pool.delete_interval = .5
|
||||
self.addCleanup(pool.stop)
|
||||
return pool
|
||||
|
||||
@ -564,8 +475,10 @@ class DBTestCase(BaseTestCase):
|
||||
self.addCleanup(app.stop)
|
||||
return app
|
||||
|
||||
def _useBuilder(self, configfile, cleanup_interval=.5):
|
||||
self.useFixture(BuilderFixture(configfile, cleanup_interval))
|
||||
def useBuilder(self, configfile, securefile=None, cleanup_interval=.5):
|
||||
self.useFixture(
|
||||
BuilderFixture(configfile, cleanup_interval, securefile)
|
||||
)
|
||||
|
||||
def setupZK(self):
|
||||
f = ZookeeperServerFixture()
|
||||
@ -587,8 +500,8 @@ class DBTestCase(BaseTestCase):
|
||||
def printZKTree(self, node):
|
||||
def join(a, b):
|
||||
if a.endswith('/'):
|
||||
return a+b
|
||||
return a+'/'+b
|
||||
return a + b
|
||||
return a + '/' + b
|
||||
|
||||
data, stat = self.zk.client.get(node)
|
||||
self.log.debug("Node: %s" % (node,))
|
||||
|
15
nodepool/tests/fixtures/clouds.yaml
vendored
Normal file
15
nodepool/tests/fixtures/clouds.yaml
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
clouds:
|
||||
fake:
|
||||
auth:
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
project_id: 'fake'
|
||||
auth_url: 'fake'
|
||||
|
||||
fake-vhd:
|
||||
auth:
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
project_id: 'fake'
|
||||
auth_url: 'fake'
|
||||
image_format: 'vhd'
|
103
nodepool/tests/fixtures/config_validate/good.yaml
vendored
103
nodepool/tests/fixtures/config_validate/good.yaml
vendored
@ -1,21 +1,9 @@
|
||||
elements-dir: /etc/nodepool/elements
|
||||
images-dir: /opt/nodepool_dib
|
||||
|
||||
cron:
|
||||
cleanup: '*/1 * * * *'
|
||||
check: '*/15 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://jenkins01.openstack.org:8888
|
||||
- tcp://jenkins02.openstack.org:8888
|
||||
- tcp://jenkins03.openstack.org:8888
|
||||
- tcp://jenkins04.openstack.org:8888
|
||||
- tcp://jenkins05.openstack.org:8888
|
||||
- tcp://jenkins06.openstack.org:8888
|
||||
- tcp://jenkins07.openstack.org:8888
|
||||
|
||||
gearman-servers:
|
||||
- host: zuul.openstack.org
|
||||
webapp:
|
||||
port: 8005
|
||||
listen_address: '0.0.0.0'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: zk1.openstack.org
|
||||
@ -24,60 +12,69 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: trusty
|
||||
image: trusty
|
||||
ready-script: configure_mirror.sh
|
||||
max-ready-age: 3600
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: cloud1
|
||||
- name: cloud2
|
||||
- name: trusty-2-node
|
||||
image: trusty
|
||||
ready-script: multinode_setup.sh
|
||||
subnodes: 1
|
||||
min-ready: 0
|
||||
providers:
|
||||
- name: cloud1
|
||||
- name: cloud2
|
||||
- name: trusty-external
|
||||
min-ready: 1
|
||||
|
||||
providers:
|
||||
- name: cloud1
|
||||
driver: openstack
|
||||
cloud: vanilla-cloud
|
||||
region-name: 'vanilla'
|
||||
service-type: 'compute'
|
||||
service-name: 'cloudServersOpenStack'
|
||||
username: '<%= username %>'
|
||||
password: '<%= password %>'
|
||||
project-id: '<%= project %>'
|
||||
auth-url: 'https://identity.example.com/v2.0/'
|
||||
boot-timeout: 120
|
||||
max-servers: 184
|
||||
max-concurrency: 10
|
||||
launch-retries: 3
|
||||
rate: 0.001
|
||||
images:
|
||||
diskimages:
|
||||
- name: trusty
|
||||
min-ram: 8192
|
||||
username: jenkins
|
||||
user-home: /home/jenkins
|
||||
private-key: /home/nodepool/.ssh/id_rsa
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 184
|
||||
auto-floating-ip: True
|
||||
labels:
|
||||
- name: trusty
|
||||
diskimage: trusty
|
||||
min-ram: 8192
|
||||
console-log: True
|
||||
- name: trusty-2-node
|
||||
diskimage: trusty
|
||||
min-ram: 8192
|
||||
boot-from-volume: True
|
||||
volume-size: 100
|
||||
|
||||
- name: cloud2
|
||||
driver: openstack
|
||||
cloud: chocolate-cloud
|
||||
region-name: 'chocolate'
|
||||
service-type: 'compute'
|
||||
service-name: 'cloudServersOpenStack'
|
||||
username: '<%= username %>'
|
||||
password: '<%= password %>'
|
||||
project-id: '<%= project %>'
|
||||
auth-url: 'https://identity.example.com/v2.0/'
|
||||
boot-timeout: 120
|
||||
max-servers: 184
|
||||
rate: 0.001
|
||||
images:
|
||||
diskimages:
|
||||
- name: trusty
|
||||
pause: False
|
||||
min-ram: 8192
|
||||
username: jenkins
|
||||
user-home: /home/jenkins
|
||||
private-key: /home/nodepool/.ssh/id_rsa
|
||||
|
||||
targets:
|
||||
- name: zuul
|
||||
connection-type: ssh
|
||||
cloud-images:
|
||||
- name: trusty-unmanaged
|
||||
config-drive: true
|
||||
- name: windows-unmanaged
|
||||
username: winzuul
|
||||
connection-type: winrm
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 184
|
||||
auto-floating-ip: False
|
||||
labels:
|
||||
- name: trusty
|
||||
diskimage: trusty
|
||||
min-ram: 8192
|
||||
- name: trusty-2-node
|
||||
diskimage: trusty
|
||||
min-ram: 8192
|
||||
- name: trusty-external
|
||||
cloud-image: trusty-unmanaged
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: trusty
|
||||
|
@ -1,22 +1,6 @@
|
||||
elements-dir: /etc/nodepool/elements
|
||||
images-dir: /opt/nodepool_dib
|
||||
|
||||
cron:
|
||||
cleanup: '*/1 * * * *'
|
||||
check: '*/15 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://jenkins01.openstack.org:8888
|
||||
- tcp://jenkins02.openstack.org:8888
|
||||
- tcp://jenkins03.openstack.org:8888
|
||||
- tcp://jenkins04.openstack.org:8888
|
||||
- tcp://jenkins05.openstack.org:8888
|
||||
- tcp://jenkins06.openstack.org:8888
|
||||
- tcp://jenkins07.openstack.org:8888
|
||||
|
||||
gearman-servers:
|
||||
- host: zuul.openstack.org
|
||||
|
||||
zookeeper-servers:
|
||||
- host: zk1.openstack.org
|
||||
port: 2181
|
||||
@ -25,15 +9,12 @@ zookeeper-servers:
|
||||
labels:
|
||||
- name: trusty
|
||||
image: trusty
|
||||
ready-script: configure_mirror.sh
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: cloud1
|
||||
- name: cloud2
|
||||
- name: trusty-2-node
|
||||
image: trusty
|
||||
ready-script: multinode_setup.sh
|
||||
subnodes: 1
|
||||
min-ready: 0
|
||||
providers:
|
||||
- name: cloud1
|
||||
@ -42,39 +23,20 @@ labels:
|
||||
providers:
|
||||
- name: cloud1
|
||||
region-name: 'vanilla'
|
||||
service-type: 'compute'
|
||||
service-name: 'cloudServersOpenStack'
|
||||
username: '<%= username %>'
|
||||
password: '<%= password %>'
|
||||
project-id: '<%= project %>'
|
||||
auth-url: 'https://identity.example.com/v2.0/'
|
||||
boot-timeout: 120
|
||||
max-servers: 184
|
||||
rate: 0.001
|
||||
images:
|
||||
- name: trusty
|
||||
min-ram: 8192
|
||||
username: jenkins
|
||||
private-key: /home/nodepool/.ssh/id_rsa
|
||||
- name: cloud2
|
||||
region-name: 'chocolate'
|
||||
service-type: 'compute'
|
||||
service-name: 'cloudServersOpenStack'
|
||||
username: '<%= username %>'
|
||||
password: '<%= password %>'
|
||||
project-id: '<%= project %>'
|
||||
auth-url: 'https://identity.example.com/v2.0/'
|
||||
boot-timeout: 120
|
||||
max-servers: 184
|
||||
rate: 0.001
|
||||
images:
|
||||
- name: trusty
|
||||
min-ram: 8192
|
||||
username: jenkins
|
||||
private-key: /home/nodepool/.ssh/id_rsa
|
||||
|
||||
targets:
|
||||
- name: zuul
|
||||
|
||||
diskimages:
|
||||
- name: trusty
|
||||
|
52
nodepool/tests/fixtures/integration.yaml
vendored
52
nodepool/tests/fixtures/integration.yaml
vendored
@ -1,52 +0,0 @@
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: localhost
|
||||
|
||||
labels:
|
||||
- name: real-label
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: real-provider
|
||||
|
||||
providers:
|
||||
- name: real-provider
|
||||
region-name: real-region
|
||||
username: 'real'
|
||||
password: 'real'
|
||||
auth-url: 'real'
|
||||
project-id: 'real'
|
||||
max-servers: 96
|
||||
pool: 'real'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Real'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
jenkins:
|
||||
url: https://jenkins.example.org/
|
||||
user: fake
|
||||
apikey: fake
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
29
nodepool/tests/fixtures/integration_noocc.yaml
vendored
Normal file
29
nodepool/tests/fixtures/integration_noocc.yaml
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: localhost
|
||||
|
||||
labels:
|
||||
- name: real-label
|
||||
min-ready: 1
|
||||
|
||||
providers:
|
||||
- name: real-provider
|
||||
region-name: real-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: real-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
flavor-name: 'Real'
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
29
nodepool/tests/fixtures/integration_occ.yaml
vendored
Normal file
29
nodepool/tests/fixtures/integration_occ.yaml
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: localhost
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 1
|
||||
|
||||
providers:
|
||||
- name: real-provider
|
||||
cloud: real-cloud
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
flavor-name: 'Real'
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
44
nodepool/tests/fixtures/integration_osc.yaml
vendored
44
nodepool/tests/fixtures/integration_osc.yaml
vendored
@ -1,44 +0,0 @@
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: localhost
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: real-provider
|
||||
|
||||
providers:
|
||||
- name: real-provider
|
||||
cloud: real-cloud
|
||||
max-servers: 96
|
||||
pool: 'real'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Real'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
53
nodepool/tests/fixtures/launcher_two_provider.yaml
vendored
Normal file
53
nodepool/tests/fixtures/launcher_two_provider.yaml
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 1
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
- name: fake-provider2
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
63
nodepool/tests/fixtures/launcher_two_provider_max_1.yaml
vendored
Normal file
63
nodepool/tests/fixtures/launcher_two_provider_max_1.yaml
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 0
|
||||
- name: fake-label2
|
||||
min-ready: 0
|
||||
- name: fake-label3
|
||||
min-ready: 0
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 1
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
- name: fake-label2
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
- name: fake-provider2
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 1
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
- name: fake-label3
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
39
nodepool/tests/fixtures/launcher_two_provider_remove.yaml
vendored
Normal file
39
nodepool/tests/fixtures/launcher_two_provider_remove.yaml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 1
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
41
nodepool/tests/fixtures/leaked_node.yaml
vendored
41
nodepool/tests/fixtures/leaked_node.yaml
vendored
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '* * * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,33 +8,23 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
|
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '* * * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,34 +8,24 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
nodepool-id: foo
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
|
57
nodepool/tests/fixtures/multiple_pools.yaml
vendored
Normal file
57
nodepool/tests/fixtures/multiple_pools.yaml
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label1
|
||||
min-ready: 1
|
||||
- name: fake-label2
|
||||
min-ready: 1
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
pools:
|
||||
- name: pool1
|
||||
max-servers: 1
|
||||
availability-zones:
|
||||
- az1
|
||||
labels:
|
||||
- name: fake-label1
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
flavor-name: 'Fake'
|
||||
|
||||
- name: pool2
|
||||
max-servers: 1
|
||||
availability-zones:
|
||||
- az2
|
||||
labels:
|
||||
- name: fake-label2
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
flavor-name: 'Fake'
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
43
nodepool/tests/fixtures/node.yaml
vendored
43
nodepool/tests/fixtures/node.yaml
vendored
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,33 +8,31 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
availability-zones:
|
||||
- az1
|
||||
networks:
|
||||
- net-name
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
flavor-name: 'Fake'
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
|
84
nodepool/tests/fixtures/node_auto_floating_ip.yaml
vendored
Normal file
84
nodepool/tests/fixtures/node_auto_floating_ip.yaml
vendored
Normal file
@ -0,0 +1,84 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label1
|
||||
min-ready: 1
|
||||
|
||||
- name: fake-label2
|
||||
min-ready: 1
|
||||
|
||||
- name: fake-label3
|
||||
min-ready: 1
|
||||
|
||||
providers:
|
||||
- name: fake-provider1
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
networks:
|
||||
- 'some-name'
|
||||
auto-floating-ip: False
|
||||
labels:
|
||||
- name: fake-label1
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
- name: fake-provider2
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
networks:
|
||||
- 'some-name'
|
||||
auto-floating-ip: True
|
||||
labels:
|
||||
- name: fake-label2
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
- name: fake-provider3
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
networks:
|
||||
- 'some-name'
|
||||
# Test default value of auto-floating-ip is True
|
||||
labels:
|
||||
- name: fake-label3
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
43
nodepool/tests/fixtures/node_az.yaml
vendored
43
nodepool/tests/fixtures/node_az.yaml
vendored
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,35 +8,29 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
availability-zones:
|
||||
- az1
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
availability-zones:
|
||||
- az1
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
flavor-name: 'Fake'
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
|
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,40 +8,32 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 2
|
||||
providers:
|
||||
- name: fake-provider
|
||||
- name: multi-fake
|
||||
image: fake-image
|
||||
ready-script: multinode_setup.sh
|
||||
subnodes: 2
|
||||
min-ready: 2
|
||||
providers:
|
||||
- name: fake-provider
|
||||
min-ready: 1
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
availability-zones:
|
||||
- az1
|
||||
networks:
|
||||
- net-name
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
flavor-name: 'Fake'
|
||||
boot-from-volume: True
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
75
nodepool/tests/fixtures/node_cmd.yaml
vendored
75
nodepool/tests/fixtures/node_cmd.yaml
vendored
@ -1,16 +1,5 @@
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -18,54 +7,46 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label1
|
||||
image: fake-image1
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider1
|
||||
- name: fake-label2
|
||||
image: fake-image2
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider2
|
||||
|
||||
providers:
|
||||
- name: fake-provider1
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
cloud: fake
|
||||
driver: fake
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image1
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
- name: fake-provider2
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
- name: fake-image2
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label1
|
||||
diskimage: fake-image1
|
||||
min-ram: 8192
|
||||
flavor-name: 'fake'
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
- name: fake-provider2
|
||||
cloud: fake
|
||||
driver: fake
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image2
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label2
|
||||
diskimage: fake-image2
|
||||
min-ram: 8192
|
||||
flavor-name: 'fake'
|
||||
|
||||
diskimages:
|
||||
- name: fake-image1
|
||||
|
39
nodepool/tests/fixtures/node_disabled_label.yaml
vendored
39
nodepool/tests/fixtures/node_disabled_label.yaml
vendored
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,33 +8,27 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 0
|
||||
providers:
|
||||
- name: fake-provider
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
flavor-name: 'fake'
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
|
39
nodepool/tests/fixtures/node_diskimage_fail.yaml
vendored
39
nodepool/tests/fixtures/node_diskimage_fail.yaml
vendored
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,33 +8,27 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
flavor-name: 'fake'
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
|
13
nodepool/tests/fixtures/node_diskimage_only.yaml
vendored
13
nodepool/tests/fixtures/node_diskimage_only.yaml
vendored
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -21,8 +10,6 @@ labels: []
|
||||
|
||||
providers: []
|
||||
|
||||
targets: []
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
formats:
|
||||
|
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,40 +8,32 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider
|
||||
- name: fake-label2
|
||||
image: fake-image2
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
- name: fake-image2
|
||||
min-ram: 8192
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
- name: fake-label2
|
||||
diskimage: fake-image2
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
|
39
nodepool/tests/fixtures/node_flavor_name.yaml
vendored
Normal file
39
nodepool/tests/fixtures/node_flavor_name.yaml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 1
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
flavor-name: Fake Flavor
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,41 +8,33 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider
|
||||
- name: fake-label2
|
||||
image: fake-image2
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pause: True
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
- name: fake-image2
|
||||
min-ram: 8192
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ram: 8192
|
||||
diskimage: fake-image
|
||||
- name: fake-label2
|
||||
diskimage: fake-image2
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
|
101
nodepool/tests/fixtures/node_ipv6.yaml
vendored
101
nodepool/tests/fixtures/node_ipv6.yaml
vendored
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,85 +8,47 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label1
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider1
|
||||
|
||||
- name: fake-label2
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider2
|
||||
|
||||
- name: fake-label3
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider3
|
||||
|
||||
providers:
|
||||
- name: fake-provider1
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'ipv6-uuid'
|
||||
ipv6-preferred: True
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
networks:
|
||||
# This activates a flag in fakeprovider to give us an ipv6
|
||||
# network
|
||||
- 'fake-ipv6-network-name'
|
||||
labels:
|
||||
- name: fake-label1
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
- name: fake-provider2
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'ipv6-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
|
||||
- name: fake-provider3
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
ipv6-preferred: True
|
||||
rate: 0.0001
|
||||
images:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
networks:
|
||||
- 'some-name'
|
||||
labels:
|
||||
- name: fake-label2
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
|
49
nodepool/tests/fixtures/node_label_provider.yaml
vendored
Normal file
49
nodepool/tests/fixtures/node_label_provider.yaml
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 1
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
- name: fake-provider2
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
40
nodepool/tests/fixtures/node_launch_retry.yaml
vendored
Normal file
40
nodepool/tests/fixtures/node_launch_retry.yaml
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 0
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
launch-retries: 2
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
41
nodepool/tests/fixtures/node_lost_requests.yaml
vendored
Normal file
41
nodepool/tests/fixtures/node_lost_requests.yaml
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 0
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
availability-zones:
|
||||
- az1
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
54
nodepool/tests/fixtures/node_many_labels.yaml
vendored
Normal file
54
nodepool/tests/fixtures/node_many_labels.yaml
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label1
|
||||
min-ready: 1
|
||||
- name: fake-label2
|
||||
min-ready: 1
|
||||
- name: fake-label3
|
||||
min-ready: 1
|
||||
- name: fake-label4
|
||||
min-ready: 1
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label1
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
- name: fake-label2
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
- name: fake-label3
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
- name: fake-label4
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
48
nodepool/tests/fixtures/node_max_ready_age.yaml
vendored
Normal file
48
nodepool/tests/fixtures/node_max_ready_age.yaml
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
max-ready-age: 2
|
||||
min-ready: 1
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
availability-zones:
|
||||
- az1
|
||||
networks:
|
||||
- net-name
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
flavor-name: 'Fake'
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
47
nodepool/tests/fixtures/node_min_ready_capacity.yaml
vendored
Normal file
47
nodepool/tests/fixtures/node_min_ready_capacity.yaml
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 0
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 1
|
||||
availability-zones:
|
||||
- az1
|
||||
networks:
|
||||
- net-name
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
flavor-name: 'Fake'
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
46
nodepool/tests/fixtures/node_net_name.yaml
vendored
46
nodepool/tests/fixtures/node_net_name.yaml
vendored
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,35 +8,26 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- name: 'fake-public-network-name'
|
||||
public: true
|
||||
- name: 'fake-private-network-name'
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
networks:
|
||||
- 'fake-public-network-name'
|
||||
- 'fake-private-network-name'
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
|
47
nodepool/tests/fixtures/node_no_min_ready.yaml
vendored
Normal file
47
nodepool/tests/fixtures/node_no_min_ready.yaml
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 0
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
availability-zones:
|
||||
- az1
|
||||
networks:
|
||||
- net-name
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
flavor-name: 'Fake'
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
39
nodepool/tests/fixtures/node_quota_cloud.yaml
vendored
Normal file
39
nodepool/tests/fixtures/node_quota_cloud.yaml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 0
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 20
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
39
nodepool/tests/fixtures/node_quota_pool_cores.yaml
vendored
Normal file
39
nodepool/tests/fixtures/node_quota_pool_cores.yaml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 0
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-cores: 8
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
39
nodepool/tests/fixtures/node_quota_pool_instances.yaml
vendored
Normal file
39
nodepool/tests/fixtures/node_quota_pool_instances.yaml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 0
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 2
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
39
nodepool/tests/fixtures/node_quota_pool_ram.yaml
vendored
Normal file
39
nodepool/tests/fixtures/node_quota_pool_ram.yaml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 0
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-ram: 16384
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
48
nodepool/tests/fixtures/node_two_image.yaml
vendored
48
nodepool/tests/fixtures/node_two_image.yaml
vendored
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,40 +8,29 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider
|
||||
- name: fake-label2
|
||||
image: fake-image2
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
- name: fake-image2
|
||||
min-ram: 8192
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
- name: fake-label2
|
||||
diskimage: fake-image2
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
|
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,33 +8,23 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
|
66
nodepool/tests/fixtures/node_two_provider.yaml
vendored
66
nodepool/tests/fixtures/node_two_provider.yaml
vendored
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,52 +8,37 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider
|
||||
- name: fake-provider2
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
- name: fake-provider2
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
|
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,45 +8,29 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
- name: fake-provider2
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images: []
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
- name: fake-provider2
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
|
35
nodepool/tests/fixtures/node_unmanaged_image.yaml
vendored
Normal file
35
nodepool/tests/fixtures/node_unmanaged_image.yaml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 1
|
||||
- name: fake-label-windows
|
||||
min-ready: 1
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
cloud-images:
|
||||
- name: fake-image
|
||||
- name: fake-image-windows
|
||||
username: zuul
|
||||
connection-type: winrm
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
cloud-image: fake-image
|
||||
min-ram: 8192
|
||||
- name: fake-label-windows
|
||||
cloud-image: fake-image-windows
|
||||
min-ram: 8192
|
72
nodepool/tests/fixtures/node_upload_fail.yaml
vendored
72
nodepool/tests/fixtures/node_upload_fail.yaml
vendored
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,53 +8,40 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 2
|
||||
providers:
|
||||
- name: fake-provider1
|
||||
- name: fake-provider2
|
||||
|
||||
providers:
|
||||
- name: fake-provider1
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 1
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
SHOULD_FAIL: 'true'
|
||||
- name: fake-provider2
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 2
|
||||
pool: 'fake'
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 2
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
- name: fake-provider2
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 2
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
|
42
nodepool/tests/fixtures/node_vhd.yaml
vendored
42
nodepool/tests/fixtures/node_vhd.yaml
vendored
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,34 +8,23 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 1
|
||||
providers:
|
||||
- name: fake-provider
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake-vhd
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 96
|
||||
pool: 'fake'
|
||||
image-type: vhd
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
|
75
nodepool/tests/fixtures/node_vhd_and_qcow2.yaml
vendored
75
nodepool/tests/fixtures/node_vhd_and_qcow2.yaml
vendored
@ -1,17 +1,6 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
cron:
|
||||
check: '*/15 * * * *'
|
||||
cleanup: '*/1 * * * *'
|
||||
|
||||
zmq-publishers:
|
||||
- tcp://localhost:8881
|
||||
|
||||
gearman-servers:
|
||||
- host: localhost
|
||||
port: {gearman_port}
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
@ -19,54 +8,38 @@ zookeeper-servers:
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
image: fake-image
|
||||
min-ready: 2
|
||||
providers:
|
||||
- name: fake-provider1
|
||||
- name: fake-provider2
|
||||
|
||||
providers:
|
||||
- name: fake-provider1
|
||||
cloud: fake-vhd
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 1
|
||||
pool: 'fake'
|
||||
image-type: vhd
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
- name: fake-provider2
|
||||
region-name: fake-region
|
||||
username: 'fake'
|
||||
password: 'fake'
|
||||
auth-url: 'fake'
|
||||
project-id: 'fake'
|
||||
max-servers: 1
|
||||
pool: 'fake'
|
||||
image-type: qcow2
|
||||
networks:
|
||||
- net-id: 'some-uuid'
|
||||
rate: 0.0001
|
||||
images:
|
||||
- name: fake-image
|
||||
min-ram: 8192
|
||||
name-filter: 'Fake'
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 2
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
targets:
|
||||
- name: fake-target
|
||||
- name: fake-provider2
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 2
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
|
47
nodepool/tests/fixtures/pause_declined_1.yaml
vendored
Normal file
47
nodepool/tests/fixtures/pause_declined_1.yaml
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 0
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 2
|
||||
availability-zones:
|
||||
- az1
|
||||
networks:
|
||||
- net-name
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
flavor-name: 'Fake'
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
47
nodepool/tests/fixtures/pause_declined_2.yaml
vendored
Normal file
47
nodepool/tests/fixtures/pause_declined_2.yaml
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 0
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 1
|
||||
availability-zones:
|
||||
- az1
|
||||
networks:
|
||||
- net-name
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
flavor-name: 'Fake'
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
8
nodepool/tests/fixtures/secure.conf
vendored
8
nodepool/tests/fixtures/secure.conf
vendored
@ -1,8 +0,0 @@
|
||||
[database]
|
||||
dburi={dburi}
|
||||
|
||||
[jenkins "fake-target"]
|
||||
user=fake
|
||||
apikey=fake
|
||||
credentials=fake
|
||||
url=http://fake-url
|
47
nodepool/tests/fixtures/secure_file_config.yaml
vendored
Normal file
47
nodepool/tests/fixtures/secure_file_config.yaml
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: invalid_host
|
||||
port: 1
|
||||
chroot: invalid_chroot
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 1
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
meta:
|
||||
key: value
|
||||
key2: value
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
availability-zones:
|
||||
- az1
|
||||
networks:
|
||||
- net-name
|
||||
labels:
|
||||
- name: fake-label
|
||||
diskimage: fake-image
|
||||
min-ram: 8192
|
||||
flavor-name: 'Fake'
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora
|
||||
- vm
|
||||
release: 21
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
4
nodepool/tests/fixtures/secure_file_secure.yaml
vendored
Normal file
4
nodepool/tests/fixtures/secure_file_secure.yaml
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
28
nodepool/tests/fixtures/unmanaged_image_provider_name.yaml
vendored
Normal file
28
nodepool/tests/fixtures/unmanaged_image_provider_name.yaml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
labels:
|
||||
- name: fake-label
|
||||
min-ready: 1
|
||||
|
||||
providers:
|
||||
- name: fake-provider
|
||||
cloud: fake
|
||||
driver: fake
|
||||
region-name: fake-region
|
||||
rate: 0.0001
|
||||
cloud-images:
|
||||
- name: fake-image
|
||||
image-name: provider-named-image
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 96
|
||||
labels:
|
||||
- name: fake-label
|
||||
cloud-image: fake-image
|
||||
min-ram: 8192
|
3
nodepool/tests/fixtures/webapp.yaml
vendored
Normal file
3
nodepool/tests/fixtures/webapp.yaml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
webapp:
|
||||
port: 8080
|
||||
listen_address: '127.0.0.1'
|
@ -1,444 +0,0 @@
|
||||
# Copyright (C) 2014 OpenStack Foundation
|
||||
#
|
||||
# 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 testscenarios
|
||||
|
||||
from nodepool import tests
|
||||
from nodepool import allocation
|
||||
|
||||
|
||||
class OneLabel(tests.AllocatorTestCase, tests.BaseTestCase):
|
||||
"""The simplest case: one each of providers, labels, and
|
||||
targets.
|
||||
|
||||
Result AGT is:
|
||||
* label1 from provider1
|
||||
"""
|
||||
|
||||
scenarios = [
|
||||
('one_node',
|
||||
dict(provider1=10, label1=1, results=[1])),
|
||||
('two_nodes',
|
||||
dict(provider1=10, label1=2, results=[2])),
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super(OneLabel, self).setUp()
|
||||
ap1 = allocation.AllocationProvider('provider1', self.provider1)
|
||||
at1 = allocation.AllocationTarget('target1')
|
||||
ar1 = allocation.AllocationRequest('label1', self.label1)
|
||||
ar1.addTarget(at1, 0)
|
||||
self.agt.append(ar1.addProvider(ap1, at1, 0)[1])
|
||||
ap1.makeGrants()
|
||||
|
||||
|
||||
class TwoLabels(tests.AllocatorTestCase, tests.BaseTestCase):
|
||||
"""Two labels from one provider.
|
||||
|
||||
Result AGTs are:
|
||||
* label1 from provider1
|
||||
* label1 from provider2
|
||||
"""
|
||||
|
||||
scenarios = [
|
||||
('one_node',
|
||||
dict(provider1=10, label1=1, label2=1, results=[1, 1])),
|
||||
('two_nodes',
|
||||
dict(provider1=10, label1=2, label2=2, results=[2, 2])),
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super(TwoLabels, self).setUp()
|
||||
ap1 = allocation.AllocationProvider('provider1', self.provider1)
|
||||
at1 = allocation.AllocationTarget('target1')
|
||||
ar1 = allocation.AllocationRequest('label1', self.label1)
|
||||
ar2 = allocation.AllocationRequest('label2', self.label2)
|
||||
ar1.addTarget(at1, 0)
|
||||
ar2.addTarget(at1, 0)
|
||||
self.agt.append(ar1.addProvider(ap1, at1, 0)[1])
|
||||
self.agt.append(ar2.addProvider(ap1, at1, 0)[1])
|
||||
ap1.makeGrants()
|
||||
|
||||
|
||||
class TwoProvidersTwoLabels(tests.AllocatorTestCase, tests.BaseTestCase):
|
||||
"""Two labels, each of which is supplied by both providers.
|
||||
|
||||
Result AGTs are:
|
||||
* label1 from provider1
|
||||
* label2 from provider1
|
||||
* label1 from provider2
|
||||
* label2 from provider2
|
||||
"""
|
||||
|
||||
scenarios = [
|
||||
('one_node',
|
||||
dict(provider1=10, provider2=10, label1=1, label2=1,
|
||||
results=[1, 1, 0, 0])),
|
||||
('two_nodes',
|
||||
dict(provider1=10, provider2=10, label1=2, label2=2,
|
||||
results=[1, 1, 1, 1])),
|
||||
('three_nodes',
|
||||
dict(provider1=10, provider2=10, label1=3, label2=3,
|
||||
results=[2, 2, 1, 1])),
|
||||
('four_nodes',
|
||||
dict(provider1=10, provider2=10, label1=4, label2=4,
|
||||
results=[2, 2, 2, 2])),
|
||||
('four_nodes_at_quota',
|
||||
dict(provider1=4, provider2=4, label1=4, label2=4,
|
||||
results=[2, 2, 2, 2])),
|
||||
('four_nodes_over_quota',
|
||||
dict(provider1=2, provider2=2, label1=4, label2=4,
|
||||
results=[1, 1, 1, 1])),
|
||||
('negative_provider',
|
||||
dict(provider1=-5, provider2=20, label1=5, label2=5,
|
||||
results=[0, 0, 5, 5])),
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super(TwoProvidersTwoLabels, self).setUp()
|
||||
ap1 = allocation.AllocationProvider('provider1', self.provider1)
|
||||
ap2 = allocation.AllocationProvider('provider2', self.provider2)
|
||||
at1 = allocation.AllocationTarget('target1')
|
||||
ar1 = allocation.AllocationRequest('label1', self.label1)
|
||||
ar2 = allocation.AllocationRequest('label2', self.label2)
|
||||
ar1.addTarget(at1, 0)
|
||||
ar2.addTarget(at1, 0)
|
||||
self.agt.append(ar1.addProvider(ap1, at1, 0)[1])
|
||||
self.agt.append(ar2.addProvider(ap1, at1, 0)[1])
|
||||
self.agt.append(ar1.addProvider(ap2, at1, 0)[1])
|
||||
self.agt.append(ar2.addProvider(ap2, at1, 0)[1])
|
||||
ap1.makeGrants()
|
||||
ap2.makeGrants()
|
||||
|
||||
|
||||
class TwoProvidersTwoLabelsOneShared(tests.AllocatorTestCase,
|
||||
tests.BaseTestCase):
|
||||
"""One label is served by both providers, the other can only come
|
||||
from one. This tests that the allocator uses the diverse provider
|
||||
to supply the label that can come from either while reserving
|
||||
nodes from the more restricted provider for the label that can
|
||||
only be supplied by it.
|
||||
|
||||
label1 is supplied by provider1 and provider2.
|
||||
label2 is supplied only by provider2.
|
||||
|
||||
Result AGTs are:
|
||||
* label1 from provider1
|
||||
* label2 from provider1
|
||||
* label2 from provider2
|
||||
"""
|
||||
|
||||
scenarios = [
|
||||
('one_node',
|
||||
dict(provider1=10, provider2=10, label1=1, label2=1,
|
||||
results=[1, 1, 0])),
|
||||
('two_nodes',
|
||||
dict(provider1=10, provider2=10, label1=2, label2=2,
|
||||
results=[2, 1, 1])),
|
||||
('three_nodes',
|
||||
dict(provider1=10, provider2=10, label1=3, label2=3,
|
||||
results=[3, 2, 1])),
|
||||
('four_nodes',
|
||||
dict(provider1=10, provider2=10, label1=4, label2=4,
|
||||
results=[4, 2, 2])),
|
||||
('four_nodes_at_quota',
|
||||
dict(provider1=4, provider2=4, label1=4, label2=4,
|
||||
results=[4, 0, 4])),
|
||||
('four_nodes_over_quota',
|
||||
dict(provider1=2, provider2=2, label1=4, label2=4,
|
||||
results=[2, 0, 2])),
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super(TwoProvidersTwoLabelsOneShared, self).setUp()
|
||||
ap1 = allocation.AllocationProvider('provider1', self.provider1)
|
||||
ap2 = allocation.AllocationProvider('provider2', self.provider2)
|
||||
at1 = allocation.AllocationTarget('target1')
|
||||
ar1 = allocation.AllocationRequest('label1', self.label1)
|
||||
ar2 = allocation.AllocationRequest('label2', self.label2)
|
||||
ar1.addTarget(at1, 0)
|
||||
ar2.addTarget(at1, 0)
|
||||
self.agt.append(ar1.addProvider(ap1, at1, 0)[1])
|
||||
self.agt.append(ar2.addProvider(ap1, at1, 0)[1])
|
||||
self.agt.append(ar2.addProvider(ap2, at1, 0)[1])
|
||||
ap1.makeGrants()
|
||||
ap2.makeGrants()
|
||||
|
||||
|
||||
class RoundRobinAllocation(tests.RoundRobinTestCase, tests.BaseTestCase):
|
||||
"""Test the round-robin behaviour of the AllocationHistory object to
|
||||
ensure fairness of distribution
|
||||
|
||||
"""
|
||||
|
||||
scenarios = [
|
||||
# * one_to_one
|
||||
#
|
||||
# test that with only one node available we cycle through the
|
||||
# available labels.
|
||||
#
|
||||
# There's a slight trick with the ordering here; makeGrants()
|
||||
# algorithm allocates proportionally from the available nodes
|
||||
# (i.e. if there's allocations for 100 and 50, then the first
|
||||
# gets twice as many of the available nodes than the second).
|
||||
# The algorithm is
|
||||
#
|
||||
# 1) add up all your peer requests
|
||||
# 2) calculate your ratio = (your_request / all_peers)
|
||||
# 3) multiples that ratio by the available nodes
|
||||
# 4) take the floor() (you can only allocate a whole node)
|
||||
#
|
||||
# So we've got 8 total requests, each requesting one node:
|
||||
#
|
||||
# label1 = 1/7 other requests = 0.142 * 1 available node = 0
|
||||
# label2 = 1/6 other requests = 0.166 * 1 available node = 0
|
||||
# label3 = 1/4 other requests = 0.25 * 1 available node = 0
|
||||
# ...
|
||||
# label7 = 1/1 other requests = 1 * 1 available node = 1
|
||||
#
|
||||
# ergo label7 is the first to be granted its request. Thus we
|
||||
# start the round-robin from there
|
||||
('one_to_one',
|
||||
dict(provider1=1, provider2=0,
|
||||
label1=1, label2=1, label3=1, label4=1,
|
||||
label5=1, label6=1, label7=1, label8=1,
|
||||
results=['label7',
|
||||
'label1',
|
||||
'label2',
|
||||
'label3',
|
||||
'label4',
|
||||
'label5',
|
||||
'label6',
|
||||
'label8',
|
||||
'label7',
|
||||
'label1',
|
||||
'label2'])),
|
||||
|
||||
# * at_quota
|
||||
#
|
||||
# Test that when at quota, every node gets allocated on every
|
||||
# round; i.e. nobody ever misses out. odds go to ap1, even to
|
||||
# ap2
|
||||
('at_quota',
|
||||
dict(provider1=4, provider2=4,
|
||||
label1=1, label2=1, label3=1, label4=1,
|
||||
label5=1, label6=1, label7=1, label8=1,
|
||||
results=[
|
||||
'label1', 'label3', 'label5', 'label7',
|
||||
'label2', 'label4', 'label6', 'label8'] * 11
|
||||
)),
|
||||
|
||||
# * big_fish_little_pond
|
||||
#
|
||||
# In this test we have one label that far outweighs the other.
|
||||
# From the description of the ratio allocation above, it can
|
||||
# swamp the allocation pool and not allow other nodes to come
|
||||
# online.
|
||||
#
|
||||
# Here with two nodes, we check that one node is dedicated to
|
||||
# the larger label request, but the second node cycles through
|
||||
# the smaller requests.
|
||||
('big_fish_little_pond',
|
||||
dict(provider1=1, provider2=1,
|
||||
label1=100, label2=1, label3=1, label4=1,
|
||||
label5=1, label6=1, label7=1, label8=1,
|
||||
# provider1 provider2
|
||||
results=['label1', 'label1', # round 1
|
||||
'label1', 'label2', # round 2
|
||||
'label1', 'label3', # ...
|
||||
'label1', 'label4',
|
||||
'label1', 'label5',
|
||||
'label1', 'label6',
|
||||
'label1', 'label7',
|
||||
'label1', 'label8',
|
||||
'label1', 'label2',
|
||||
'label1', 'label3',
|
||||
'label1', 'label4'])),
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super(RoundRobinAllocation, self).setUp()
|
||||
|
||||
ah = allocation.AllocationHistory()
|
||||
|
||||
def do_it():
|
||||
ap1 = allocation.AllocationProvider('provider1', self.provider1)
|
||||
ap2 = allocation.AllocationProvider('provider2', self.provider2)
|
||||
|
||||
at1 = allocation.AllocationTarget('target1')
|
||||
|
||||
ars = []
|
||||
ars.append(allocation.AllocationRequest('label1', self.label1, ah))
|
||||
ars.append(allocation.AllocationRequest('label2', self.label2, ah))
|
||||
ars.append(allocation.AllocationRequest('label3', self.label3, ah))
|
||||
ars.append(allocation.AllocationRequest('label4', self.label4, ah))
|
||||
ars.append(allocation.AllocationRequest('label5', self.label5, ah))
|
||||
ars.append(allocation.AllocationRequest('label6', self.label6, ah))
|
||||
ars.append(allocation.AllocationRequest('label7', self.label7, ah))
|
||||
ars.append(allocation.AllocationRequest('label8', self.label8, ah))
|
||||
|
||||
# each request to one target, and can be satisfied by both
|
||||
# providers
|
||||
for ar in ars:
|
||||
ar.addTarget(at1, 0)
|
||||
ar.addProvider(ap1, at1, 0)
|
||||
ar.addProvider(ap2, at1, 0)
|
||||
|
||||
ap1.makeGrants()
|
||||
for g in ap1.grants:
|
||||
self.allocations.append(g.request.name)
|
||||
ap2.makeGrants()
|
||||
for g in ap2.grants:
|
||||
self.allocations.append(g.request.name)
|
||||
|
||||
ah.grantsDone()
|
||||
|
||||
# run the test several times to make sure we bounce around
|
||||
# enough
|
||||
for i in range(0, 11):
|
||||
do_it()
|
||||
|
||||
|
||||
class RoundRobinFixedProvider(tests.RoundRobinTestCase, tests.BaseTestCase):
|
||||
"""Test that round-robin behaviour exists when we have a more complex
|
||||
situation where some nodes can only be provided by some providers
|
||||
|
||||
* label1 is only able to be allocated from provider1
|
||||
* label8 is only able to be allocated from provider2
|
||||
"""
|
||||
|
||||
scenarios = [
|
||||
# * fixed_even
|
||||
#
|
||||
# What we see below is an edge case:
|
||||
#
|
||||
# Below, label1 always gets chosen because for provider1.
|
||||
# This is because label1 is requesting 1.0 nodes (it can only
|
||||
# run on provider1) and all the other labels are requesting
|
||||
# only 0.5 of a node (they can run on either and no
|
||||
# allocations have been made yet). We do actually grant in a
|
||||
# round-robin fashion, but int(0.5) == 0 so no node gets
|
||||
# allocated. We fall back to the ratio calculation and label1
|
||||
# wins.
|
||||
#
|
||||
# However, after provider1.makeGrants(), the other labels
|
||||
# increase their request on the remaning provider2 to their
|
||||
# full 1.0 nodes. Now the "fight" starts and we allocate in
|
||||
# the round-robin fashion.
|
||||
('fixed_even',
|
||||
dict(provider1=1, provider2=1,
|
||||
label1=1, label2=1, label3=1, label4=1,
|
||||
label5=1, label6=1, label7=1, label8=1,
|
||||
# provider1 provider2
|
||||
results=['label1', 'label6', # round 1
|
||||
'label1', 'label8', # round 2
|
||||
'label1', 'label2', # ...
|
||||
'label1', 'label3',
|
||||
'label1', 'label4',
|
||||
'label1', 'label5',
|
||||
'label1', 'label7',
|
||||
'label1', 'label6',
|
||||
'label1', 'label8',
|
||||
'label1', 'label2',
|
||||
'label1', 'label3'])),
|
||||
|
||||
# * over_subscribed
|
||||
#
|
||||
# In contrast to above, any grant made will be satisfied. We
|
||||
# see that the fixed node label1 and label8 do not get as full
|
||||
# a share as the non-fixed nodes -- but they do round-robin
|
||||
# with the other requests. Fixing this is left as an exercise
|
||||
# for the reader :)
|
||||
('over_subscribed',
|
||||
dict(provider1=1, provider2=1,
|
||||
label1=20, label2=20, label3=20, label4=20,
|
||||
label5=20, label6=20, label7=20, label8=20,
|
||||
results=['label1', 'label6',
|
||||
'label2', 'label8',
|
||||
'label3', 'label3',
|
||||
'label4', 'label4',
|
||||
'label5', 'label5',
|
||||
'label7', 'label7',
|
||||
'label1', 'label6',
|
||||
'label2', 'label8',
|
||||
'label3', 'label3',
|
||||
'label4', 'label4',
|
||||
'label5', 'label5'])),
|
||||
|
||||
# * even
|
||||
#
|
||||
# When there's enough nodes to go around, we expect everyone
|
||||
# to be fully satisifed with label1 on provider1 and label8
|
||||
# on provider2 as required
|
||||
('even',
|
||||
dict(provider1=4, provider2=4,
|
||||
label1=1, label2=1, label3=1, label4=1,
|
||||
label5=1, label6=1, label7=1, label8=1,
|
||||
results=[
|
||||
'label1', 'label2', 'label4', 'label6',
|
||||
'label8', 'label3', 'label5', 'label7'] * 11))]
|
||||
|
||||
def setUp(self):
|
||||
super(RoundRobinFixedProvider, self).setUp()
|
||||
|
||||
ah = allocation.AllocationHistory()
|
||||
|
||||
def do_it():
|
||||
ap1 = allocation.AllocationProvider('provider1', self.provider1)
|
||||
ap2 = allocation.AllocationProvider('provider2', self.provider2)
|
||||
|
||||
at1 = allocation.AllocationTarget('target1')
|
||||
|
||||
ars = []
|
||||
ars.append(allocation.AllocationRequest('label1', self.label1, ah))
|
||||
ars.append(allocation.AllocationRequest('label2', self.label2, ah))
|
||||
ars.append(allocation.AllocationRequest('label3', self.label3, ah))
|
||||
ars.append(allocation.AllocationRequest('label4', self.label4, ah))
|
||||
ars.append(allocation.AllocationRequest('label5', self.label5, ah))
|
||||
ars.append(allocation.AllocationRequest('label6', self.label6, ah))
|
||||
ars.append(allocation.AllocationRequest('label7', self.label7, ah))
|
||||
ars.append(allocation.AllocationRequest('label8', self.label8, ah))
|
||||
|
||||
# first ar can only go to provider1, the last only to
|
||||
# provider2
|
||||
ars[0].addTarget(at1, 0)
|
||||
ars[0].addProvider(ap1, at1, 0)
|
||||
ars[-1].addTarget(at1, 0)
|
||||
ars[-1].addProvider(ap2, at1, 0)
|
||||
|
||||
# the rest can go anywhere
|
||||
for ar in ars[1:-1]:
|
||||
ar.addTarget(at1, 0)
|
||||
ar.addProvider(ap1, at1, 0)
|
||||
ar.addProvider(ap2, at1, 0)
|
||||
|
||||
ap1.makeGrants()
|
||||
for g in ap1.grants:
|
||||
self.allocations.append(g.request.name)
|
||||
|
||||
ap2.makeGrants()
|
||||
for g in ap2.grants:
|
||||
self.allocations.append(g.request.name)
|
||||
|
||||
ah.grantsDone()
|
||||
|
||||
# run the test several times to make sure we bounce around
|
||||
# enough
|
||||
for i in range(0, 11):
|
||||
do_it()
|
||||
|
||||
|
||||
def load_tests(loader, in_tests, pattern):
|
||||
return testscenarios.load_tests_apply_scenarios(loader, in_tests, pattern)
|
@ -14,9 +14,11 @@
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import fixtures
|
||||
|
||||
from nodepool import builder, exceptions, fakeprovider, tests
|
||||
from nodepool import builder, exceptions, tests
|
||||
from nodepool.driver.fake import provider as fakeprovider
|
||||
from nodepool import zk
|
||||
|
||||
|
||||
@ -84,7 +86,9 @@ class TestNodepoolBuilderDibImage(tests.BaseTestCase):
|
||||
image = builder.DibImageFile('myid1234')
|
||||
self.assertRaises(exceptions.BuilderError, image.to_path, '/imagedir/')
|
||||
|
||||
|
||||
class TestNodePoolBuilder(tests.DBTestCase):
|
||||
|
||||
def test_start_stop(self):
|
||||
config = self.setup_config('node.yaml')
|
||||
nb = builder.NodePoolBuilder(config)
|
||||
@ -94,6 +98,18 @@ class TestNodePoolBuilder(tests.DBTestCase):
|
||||
nb.start()
|
||||
nb.stop()
|
||||
|
||||
def test_builder_id_file(self):
|
||||
configfile = self.setup_config('node.yaml')
|
||||
self.useBuilder(configfile)
|
||||
path = os.path.join(self._config_images_dir.path, 'builder_id.txt')
|
||||
|
||||
# Validate the unique ID file exists and contents are what we expect
|
||||
self.assertTrue(os.path.exists(path))
|
||||
with open(path, "r") as f:
|
||||
the_id = f.read()
|
||||
obj = uuid.UUID(the_id, version=4)
|
||||
self.assertEqual(the_id, str(obj))
|
||||
|
||||
def test_image_upload_fail(self):
|
||||
"""Test that image upload fails are handled properly."""
|
||||
|
||||
@ -104,20 +120,18 @@ class TestNodePoolBuilder(tests.DBTestCase):
|
||||
return fake_client
|
||||
|
||||
self.useFixture(fixtures.MonkeyPatch(
|
||||
'nodepool.provider_manager.FakeProviderManager._getClient',
|
||||
'nodepool.driver.fake.provider.FakeProvider._getClient',
|
||||
get_fake_client))
|
||||
self.useFixture(fixtures.MonkeyPatch(
|
||||
'nodepool.nodepool._get_one_cloud',
|
||||
fakeprovider.fake_get_one_cloud))
|
||||
|
||||
configfile = self.setup_config('node.yaml')
|
||||
pool = self.useNodepool(configfile, watermark_sleep=1)
|
||||
# NOTE(pabelanger): Disable CleanupWorker thread for nodepool-builder
|
||||
# as we currently race it to validate our failed uploads.
|
||||
self._useBuilder(configfile, cleanup_interval=0)
|
||||
self.useBuilder(configfile, cleanup_interval=0)
|
||||
pool.start()
|
||||
self.waitForImage('fake-provider', 'fake-image')
|
||||
self.waitForNodes(pool)
|
||||
nodes = self.waitForNodes('fake-label')
|
||||
self.assertEqual(len(nodes), 1)
|
||||
|
||||
newest_builds = self.zk.getMostRecentBuilds(1, 'fake-image',
|
||||
state=zk.READY)
|
||||
@ -129,32 +143,33 @@ class TestNodePoolBuilder(tests.DBTestCase):
|
||||
|
||||
def test_provider_addition(self):
|
||||
configfile = self.setup_config('node.yaml')
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
self.waitForImage('fake-provider', 'fake-image')
|
||||
self.replace_config(configfile, 'node_two_provider.yaml')
|
||||
self.waitForImage('fake-provider2', 'fake-image')
|
||||
|
||||
def test_provider_removal(self):
|
||||
configfile = self.setup_config('node_two_provider.yaml')
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
self.waitForImage('fake-provider', 'fake-image')
|
||||
self.waitForImage('fake-provider2', 'fake-image')
|
||||
image = self.zk.getMostRecentImageUpload('fake-provider', 'fake-image')
|
||||
self.replace_config(configfile, 'node_two_provider_remove.yaml')
|
||||
self.waitForImageDeletion('fake-provider2', 'fake-image')
|
||||
image2 = self.zk.getMostRecentImageUpload('fake-provider', 'fake-image')
|
||||
image2 = self.zk.getMostRecentImageUpload('fake-provider',
|
||||
'fake-image')
|
||||
self.assertEqual(image, image2)
|
||||
|
||||
def test_image_addition(self):
|
||||
configfile = self.setup_config('node.yaml')
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
self.waitForImage('fake-provider', 'fake-image')
|
||||
self.replace_config(configfile, 'node_two_image.yaml')
|
||||
self.waitForImage('fake-provider', 'fake-image2')
|
||||
|
||||
def test_image_removal(self):
|
||||
configfile = self.setup_config('node_two_image.yaml')
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
self.waitForImage('fake-provider', 'fake-image')
|
||||
self.waitForImage('fake-provider', 'fake-image2')
|
||||
self.replace_config(configfile, 'node_two_image_remove.yaml')
|
||||
@ -166,7 +181,7 @@ class TestNodePoolBuilder(tests.DBTestCase):
|
||||
|
||||
def _test_image_rebuild_age(self, expire=86400):
|
||||
configfile = self.setup_config('node.yaml')
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
build = self.waitForBuild('fake-image', '0000000001')
|
||||
image = self.waitForImage('fake-provider', 'fake-image')
|
||||
# Expire rebuild-age (default: 1day) to force a new build.
|
||||
@ -244,7 +259,7 @@ class TestNodePoolBuilder(tests.DBTestCase):
|
||||
|
||||
def test_cleanup_hard_upload_fails(self):
|
||||
configfile = self.setup_config('node.yaml')
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
self.waitForImage('fake-provider', 'fake-image')
|
||||
|
||||
upload = self.zk.getUploads('fake-image', '0000000001',
|
||||
@ -269,7 +284,7 @@ class TestNodePoolBuilder(tests.DBTestCase):
|
||||
|
||||
def test_cleanup_failed_image_build(self):
|
||||
configfile = self.setup_config('node_diskimage_fail.yaml')
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
# NOTE(pabelanger): We are racing here, but don't really care. We just
|
||||
# need our first image build to fail.
|
||||
self.replace_config(configfile, 'node.yaml')
|
||||
@ -279,5 +294,5 @@ class TestNodePoolBuilder(tests.DBTestCase):
|
||||
|
||||
def test_diskimage_build_only(self):
|
||||
configfile = self.setup_config('node_diskimage_only.yaml')
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
self.waitForBuild('fake-image', '0000000001')
|
||||
|
@ -27,12 +27,15 @@ from nodepool import zk
|
||||
|
||||
|
||||
class TestNodepoolCMD(tests.DBTestCase):
|
||||
def setUp(self):
|
||||
super(TestNodepoolCMD, self).setUp()
|
||||
|
||||
def patch_argv(self, *args):
|
||||
argv = ["nodepool", "-s", self.secure_conf]
|
||||
argv = ["nodepool"]
|
||||
argv.extend(args)
|
||||
self.useFixture(fixtures.MonkeyPatch('sys.argv', argv))
|
||||
|
||||
def assert_listed(self, configfile, cmd, col, val, count):
|
||||
def assert_listed(self, configfile, cmd, col, val, count, col_count=0):
|
||||
log = logging.getLogger("tests.PrettyTableMock")
|
||||
self.patch_argv("-c", configfile, *cmd)
|
||||
with mock.patch('prettytable.PrettyTable.add_row') as m_add_row:
|
||||
@ -41,13 +44,16 @@ class TestNodepoolCMD(tests.DBTestCase):
|
||||
# Find add_rows with the status were looking for
|
||||
for args, kwargs in m_add_row.call_args_list:
|
||||
row = args[0]
|
||||
if col_count:
|
||||
self.assertEquals(len(row), col_count)
|
||||
log.debug(row)
|
||||
if row[col] == val:
|
||||
rows_with_val += 1
|
||||
self.assertEquals(rows_with_val, count)
|
||||
|
||||
def assert_alien_images_listed(self, configfile, image_cnt, image_id):
|
||||
self.assert_listed(configfile, ['alien-image-list'], 2, image_id, image_cnt)
|
||||
self.assert_listed(configfile, ['alien-image-list'], 2, image_id,
|
||||
image_cnt)
|
||||
|
||||
def assert_alien_images_empty(self, configfile):
|
||||
self.assert_alien_images_listed(configfile, 0, 0)
|
||||
@ -55,8 +61,16 @@ class TestNodepoolCMD(tests.DBTestCase):
|
||||
def assert_images_listed(self, configfile, image_cnt, status="ready"):
|
||||
self.assert_listed(configfile, ['image-list'], 6, status, image_cnt)
|
||||
|
||||
def assert_nodes_listed(self, configfile, node_cnt, status="ready"):
|
||||
self.assert_listed(configfile, ['list'], 10, status, node_cnt)
|
||||
def assert_nodes_listed(self, configfile, node_cnt, status="ready",
|
||||
detail=False, validate_col_count=False):
|
||||
cmd = ['list']
|
||||
col_count = 9
|
||||
if detail:
|
||||
cmd += ['--detail']
|
||||
col_count = 17
|
||||
if not validate_col_count:
|
||||
col_count = 0
|
||||
self.assert_listed(configfile, cmd, 6, status, node_cnt, col_count)
|
||||
|
||||
def test_image_list_empty(self):
|
||||
self.assert_images_listed(self.setup_config("node_cmd.yaml"), 0)
|
||||
@ -72,7 +86,7 @@ class TestNodepoolCMD(tests.DBTestCase):
|
||||
|
||||
def test_image_delete(self):
|
||||
configfile = self.setup_config("node.yaml")
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
self.waitForImage('fake-provider', 'fake-image')
|
||||
image = self.zk.getMostRecentImageUpload('fake-image', 'fake-provider')
|
||||
self.patch_argv("-c", configfile, "image-delete",
|
||||
@ -84,20 +98,9 @@ class TestNodepoolCMD(tests.DBTestCase):
|
||||
self.waitForUploadRecordDeletion('fake-provider', 'fake-image',
|
||||
image.build_id, image.id)
|
||||
|
||||
def test_alien_list_fail(self):
|
||||
def fail_list(self):
|
||||
raise RuntimeError('Fake list error')
|
||||
self.useFixture(fixtures.MonkeyPatch(
|
||||
'nodepool.fakeprovider.FakeOpenStackCloud.list_servers',
|
||||
fail_list))
|
||||
|
||||
configfile = self.setup_config("node_cmd.yaml")
|
||||
self.patch_argv("-c", configfile, "alien-list")
|
||||
nodepoolcmd.main()
|
||||
|
||||
def test_alien_image_list_empty(self):
|
||||
configfile = self.setup_config("node.yaml")
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
self.waitForImage('fake-provider', 'fake-image')
|
||||
self.patch_argv("-c", configfile, "alien-image-list")
|
||||
nodepoolcmd.main()
|
||||
@ -107,7 +110,7 @@ class TestNodepoolCMD(tests.DBTestCase):
|
||||
def fail_list(self):
|
||||
raise RuntimeError('Fake list error')
|
||||
self.useFixture(fixtures.MonkeyPatch(
|
||||
'nodepool.fakeprovider.FakeOpenStackCloud.list_servers',
|
||||
'nodepool.driver.fake.provider.FakeOpenStackCloud.list_servers',
|
||||
fail_list))
|
||||
|
||||
configfile = self.setup_config("node_cmd.yaml")
|
||||
@ -116,12 +119,23 @@ class TestNodepoolCMD(tests.DBTestCase):
|
||||
|
||||
def test_list_nodes(self):
|
||||
configfile = self.setup_config('node.yaml')
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
pool = self.useNodepool(configfile, watermark_sleep=1)
|
||||
pool.start()
|
||||
self.waitForImage('fake-provider', 'fake-image')
|
||||
self.waitForNodes(pool)
|
||||
self.assert_nodes_listed(configfile, 1)
|
||||
self.waitForNodes('fake-label')
|
||||
self.assert_nodes_listed(configfile, 1, detail=False,
|
||||
validate_col_count=True)
|
||||
|
||||
def test_list_nodes_detail(self):
|
||||
configfile = self.setup_config('node.yaml')
|
||||
self.useBuilder(configfile)
|
||||
pool = self.useNodepool(configfile, watermark_sleep=1)
|
||||
pool.start()
|
||||
self.waitForImage('fake-provider', 'fake-image')
|
||||
self.waitForNodes('fake-label')
|
||||
self.assert_nodes_listed(configfile, 1, detail=True,
|
||||
validate_col_count=True)
|
||||
|
||||
def test_config_validate(self):
|
||||
config = os.path.join(os.path.dirname(tests.__file__),
|
||||
@ -131,13 +145,13 @@ class TestNodepoolCMD(tests.DBTestCase):
|
||||
|
||||
def test_dib_image_list(self):
|
||||
configfile = self.setup_config('node.yaml')
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
self.waitForImage('fake-provider', 'fake-image')
|
||||
self.assert_listed(configfile, ['dib-image-list'], 4, zk.READY, 1)
|
||||
|
||||
def test_dib_image_build_pause(self):
|
||||
configfile = self.setup_config('node_diskimage_pause.yaml')
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
self.patch_argv("-c", configfile, "image-build", "fake-image")
|
||||
with testtools.ExpectedException(Exception):
|
||||
nodepoolcmd.main()
|
||||
@ -145,19 +159,21 @@ class TestNodepoolCMD(tests.DBTestCase):
|
||||
|
||||
def test_dib_image_pause(self):
|
||||
configfile = self.setup_config('node_diskimage_pause.yaml')
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
pool = self.useNodepool(configfile, watermark_sleep=1)
|
||||
pool.start()
|
||||
self.waitForNodes(pool)
|
||||
nodes = self.waitForNodes('fake-label2')
|
||||
self.assertEqual(len(nodes), 1)
|
||||
self.assert_listed(configfile, ['dib-image-list'], 1, 'fake-image', 0)
|
||||
self.assert_listed(configfile, ['dib-image-list'], 1, 'fake-image2', 1)
|
||||
|
||||
def test_dib_image_upload_pause(self):
|
||||
configfile = self.setup_config('node_image_upload_pause.yaml')
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
pool = self.useNodepool(configfile, watermark_sleep=1)
|
||||
pool.start()
|
||||
self.waitForNodes(pool)
|
||||
nodes = self.waitForNodes('fake-label2')
|
||||
self.assertEqual(len(nodes), 1)
|
||||
# Make sure diskimages were built.
|
||||
self.assert_listed(configfile, ['dib-image-list'], 1, 'fake-image', 1)
|
||||
self.assert_listed(configfile, ['dib-image-list'], 1, 'fake-image2', 1)
|
||||
@ -168,10 +184,11 @@ class TestNodepoolCMD(tests.DBTestCase):
|
||||
def test_dib_image_delete(self):
|
||||
configfile = self.setup_config('node.yaml')
|
||||
pool = self.useNodepool(configfile, watermark_sleep=1)
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
pool.start()
|
||||
self.waitForImage('fake-provider', 'fake-image')
|
||||
self.waitForNodes(pool)
|
||||
nodes = self.waitForNodes('fake-label')
|
||||
self.assertEqual(len(nodes), 1)
|
||||
# Check the image exists
|
||||
self.assert_listed(configfile, ['dib-image-list'], 4, zk.READY, 1)
|
||||
builds = self.zk.getMostRecentBuilds(1, 'fake-image', zk.READY)
|
||||
@ -187,52 +204,67 @@ class TestNodepoolCMD(tests.DBTestCase):
|
||||
def test_hold(self):
|
||||
configfile = self.setup_config('node.yaml')
|
||||
pool = self.useNodepool(configfile, watermark_sleep=1)
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
pool.start()
|
||||
self.waitForImage('fake-provider', 'fake-image')
|
||||
self.waitForNodes(pool)
|
||||
nodes = self.waitForNodes('fake-label')
|
||||
node_id = nodes[0].id
|
||||
# Assert one node exists and it is node 1 in a ready state.
|
||||
self.assert_listed(configfile, ['list'], 0, 1, 1)
|
||||
self.assert_listed(configfile, ['list'], 0, node_id, 1)
|
||||
self.assert_nodes_listed(configfile, 1, zk.READY)
|
||||
# Hold node 1
|
||||
self.patch_argv('-c', configfile, 'hold', '1')
|
||||
# Hold node 0000000000
|
||||
self.patch_argv(
|
||||
'-c', configfile, 'hold', node_id, '--reason', 'testing')
|
||||
nodepoolcmd.main()
|
||||
# Assert the state changed to HOLD
|
||||
self.assert_listed(configfile, ['list'], 0, 1, 1)
|
||||
self.assert_listed(configfile, ['list'], 0, node_id, 1)
|
||||
self.assert_nodes_listed(configfile, 1, 'hold')
|
||||
|
||||
def test_delete(self):
|
||||
configfile = self.setup_config('node.yaml')
|
||||
pool = self.useNodepool(configfile, watermark_sleep=1)
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
pool.start()
|
||||
self.waitForImage('fake-provider', 'fake-image')
|
||||
self.waitForNodes(pool)
|
||||
# Assert one node exists and it is node 1 in a ready state.
|
||||
self.assert_listed(configfile, ['list'], 0, 1, 1)
|
||||
nodes = self.waitForNodes('fake-label')
|
||||
self.assertEqual(len(nodes), 1)
|
||||
|
||||
# Assert one node exists and it is nodes[0].id in a ready state.
|
||||
self.assert_listed(configfile, ['list'], 0, nodes[0].id, 1)
|
||||
self.assert_nodes_listed(configfile, 1, zk.READY)
|
||||
# Delete node 1
|
||||
self.assert_listed(configfile, ['delete', '1'], 10, 'delete', 1)
|
||||
|
||||
# Delete node
|
||||
self.patch_argv('-c', configfile, 'delete', nodes[0].id)
|
||||
nodepoolcmd.main()
|
||||
self.waitForNodeDeletion(nodes[0])
|
||||
|
||||
# Assert the node is gone
|
||||
self.assert_listed(configfile, ['list'], 0, nodes[0].id, 0)
|
||||
|
||||
def test_delete_now(self):
|
||||
configfile = self.setup_config('node.yaml')
|
||||
pool = self.useNodepool(configfile, watermark_sleep=1)
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
pool.start()
|
||||
self.waitForImage( 'fake-provider', 'fake-image')
|
||||
self.waitForNodes(pool)
|
||||
self.waitForImage('fake-provider', 'fake-image')
|
||||
nodes = self.waitForNodes('fake-label')
|
||||
self.assertEqual(len(nodes), 1)
|
||||
|
||||
# Assert one node exists and it is node 1 in a ready state.
|
||||
self.assert_listed(configfile, ['list'], 0, 1, 1)
|
||||
self.assert_listed(configfile, ['list'], 0, nodes[0].id, 1)
|
||||
self.assert_nodes_listed(configfile, 1, zk.READY)
|
||||
# Delete node 1
|
||||
self.patch_argv('-c', configfile, 'delete', '--now', '1')
|
||||
|
||||
# Delete node
|
||||
self.patch_argv('-c', configfile, 'delete', '--now', nodes[0].id)
|
||||
nodepoolcmd.main()
|
||||
self.waitForNodeDeletion(nodes[0])
|
||||
|
||||
# Assert the node is gone
|
||||
self.assert_listed(configfile, ['list'], 0, 1, 0)
|
||||
self.assert_listed(configfile, ['list'], 0, nodes[0].id, 0)
|
||||
|
||||
def test_image_build(self):
|
||||
configfile = self.setup_config('node.yaml')
|
||||
self._useBuilder(configfile)
|
||||
self.useBuilder(configfile)
|
||||
|
||||
# wait for the scheduled build to arrive
|
||||
self.waitForImage('fake-provider', 'fake-image')
|
||||
@ -246,19 +278,25 @@ class TestNodepoolCMD(tests.DBTestCase):
|
||||
self.waitForImage('fake-provider', 'fake-image', [image])
|
||||
self.assert_listed(configfile, ['dib-image-list'], 4, zk.READY, 2)
|
||||
|
||||
def test_job_create(self):
|
||||
def test_request_list(self):
|
||||
configfile = self.setup_config('node.yaml')
|
||||
self.patch_argv("-c", configfile, "job-create", "fake-job",
|
||||
"--hold-on-failure", "1")
|
||||
nodepoolcmd.main()
|
||||
self.assert_listed(configfile, ['job-list'], 2, 1, 1)
|
||||
pool = self.useNodepool(configfile, watermark_sleep=1)
|
||||
self.useBuilder(configfile)
|
||||
pool.start()
|
||||
self.waitForImage('fake-provider', 'fake-image')
|
||||
nodes = self.waitForNodes('fake-label')
|
||||
self.assertEqual(len(nodes), 1)
|
||||
|
||||
def test_job_delete(self):
|
||||
configfile = self.setup_config('node.yaml')
|
||||
self.patch_argv("-c", configfile, "job-create", "fake-job",
|
||||
"--hold-on-failure", "1")
|
||||
nodepoolcmd.main()
|
||||
self.assert_listed(configfile, ['job-list'], 2, 1, 1)
|
||||
self.patch_argv("-c", configfile, "job-delete", "1")
|
||||
nodepoolcmd.main()
|
||||
self.assert_listed(configfile, ['job-list'], 0, 1, 0)
|
||||
req = zk.NodeRequest()
|
||||
req.state = zk.PENDING # so it will be ignored
|
||||
req.node_types = ['fake-label']
|
||||
req.requestor = 'test_request_list'
|
||||
self.zk.storeNodeRequest(req)
|
||||
|
||||
self.assert_listed(configfile, ['request-list'], 0, req.id, 1)
|
||||
|
||||
def test_without_argument(self):
|
||||
configfile = self.setup_config("node_cmd.yaml")
|
||||
self.patch_argv("-c", configfile)
|
||||
result = nodepoolcmd.main()
|
||||
self.assertEqual(1, result)
|
||||
|
1005
nodepool/tests/test_launcher.py
Normal file
1005
nodepool/tests/test_launcher.py
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user