Test OpenStack server instance enrollment
A basic test to check that a spawned instance will be added to and than deleted from FreeIPA. This also fixes the novajoin-install script to work by default on devstack. Change-Id: Id7e940360ade74d605fef9004c6a5454790c55a4
This commit is contained in:
parent
47ce2f7136
commit
fe72231faa
25
README.rst
25
README.rst
|
@ -107,10 +107,16 @@ novajoin REST service and enable notifications in
|
||||||
vendordata_dynamic_read_timeout = 30
|
vendordata_dynamic_read_timeout = 30
|
||||||
vendordata_jsonfile_path = /etc/novajoin/cloud-config-novajoin.json
|
vendordata_jsonfile_path = /etc/novajoin/cloud-config-novajoin.json
|
||||||
|
|
||||||
[oslo_messaging_notifications]
|
[notifications]
|
||||||
notification_driver = messaging
|
|
||||||
notification_topic = notifications,novajoin_notifications
|
|
||||||
notify_on_state_change = vm_state
|
notify_on_state_change = vm_state
|
||||||
|
notification_format = unversioned
|
||||||
|
|
||||||
|
[oslo_messaging_notifications]
|
||||||
|
...
|
||||||
|
topics=notifications,novajoin_notifications
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Notifications have to be also enabled and configured on nova computes!
|
||||||
|
|
||||||
Novajoin enables keystone authentication by default, as seen in
|
Novajoin enables keystone authentication by default, as seen in
|
||||||
**/etc/novajoin/join-api-paste.ini**. So credentials need to be set for
|
**/etc/novajoin/join-api-paste.ini**. So credentials need to be set for
|
||||||
|
@ -203,9 +209,18 @@ Notification listener Configuration
|
||||||
===================================
|
===================================
|
||||||
|
|
||||||
The only special configuration needed here is to configure nova to
|
The only special configuration needed here is to configure nova to
|
||||||
send notifications to the novajoin topic in /etc/nova/nova.conf:
|
send notifications to the novajoin topic in /etc/nova/nova.conf::
|
||||||
|
|
||||||
notification_topic = notifications,novajoin_notifications
|
[notifications]
|
||||||
|
notify_on_state_change = vm_state
|
||||||
|
notification_format = unversioned
|
||||||
|
|
||||||
|
[oslo_messaging_notifications]
|
||||||
|
...
|
||||||
|
topics=notifications,novajoin_notifications
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Notifications have to be also enabled and configured on nova computes!
|
||||||
|
|
||||||
If you simply use notifications and ceilometer is running then the
|
If you simply use notifications and ceilometer is running then the
|
||||||
notifications will be roughly split between the two services in a
|
notifications will be roughly split between the two services in a
|
||||||
|
|
|
@ -17,6 +17,7 @@ import os
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
import six
|
||||||
from six.moves import http_client
|
from six.moves import http_client
|
||||||
|
|
||||||
|
|
||||||
|
@ -481,6 +482,14 @@ class IPAClient(IPANovaJoinBase):
|
||||||
result = self._call_ipa('service_find', *params, **service_args)
|
result = self._call_ipa('service_find', *params, **service_args)
|
||||||
return result['count'] > 0
|
return result['count'] > 0
|
||||||
|
|
||||||
|
def find_host(self, hostname):
|
||||||
|
"""Return True if this host exists"""
|
||||||
|
LOG.debug('Checking if host ' + hostname + ' exists')
|
||||||
|
params = []
|
||||||
|
service_args = {'fqdn': six.text_type(hostname)}
|
||||||
|
result = self._call_ipa('host_find', *params, **service_args)
|
||||||
|
return result['count'] > 0
|
||||||
|
|
||||||
def delete_service(self, principal, batch=True):
|
def delete_service(self, principal, batch=True):
|
||||||
LOG.debug('Deleting service: ' + principal)
|
LOG.debug('Deleting service: ' + principal)
|
||||||
params = [principal]
|
params = [principal]
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
# Copyright 2018 Red Hat, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Tests enrollment of new OpenStack VMs in FreeIPA.
|
||||||
|
|
||||||
|
The test uses the default demo project and credentials and assumes there is a
|
||||||
|
centos-image present in Glance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import testtools
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import openstack
|
||||||
|
from oslo_service import loopingcall
|
||||||
|
|
||||||
|
from novajoin import config
|
||||||
|
from novajoin.ipa import IPAClient
|
||||||
|
|
||||||
|
|
||||||
|
CONF = config.CONF
|
||||||
|
|
||||||
|
EXAMPLE_DOMAIN = '.example.test'
|
||||||
|
TEST_IMAGE = 'centos-image'
|
||||||
|
TEST_IMAGE_USER = 'centos'
|
||||||
|
TEST_INSTANCE = str(uuid.uuid4())
|
||||||
|
TEST_KEY = str(uuid.uuid4())
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnrollment(testtools.TestCase):
|
||||||
|
"""Do a live test against a Devstack installation.
|
||||||
|
|
||||||
|
This requires:
|
||||||
|
- Devstack running on localhost
|
||||||
|
- novajoin configured and running
|
||||||
|
- centos-image present in Glance
|
||||||
|
|
||||||
|
This will add and remove server instances.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestEnrollment, self).setUp()
|
||||||
|
CONF.keytab = '/tmp/test.keytab'
|
||||||
|
if not os.path.isfile(CONF.keytab):
|
||||||
|
CONF.keytab = '/etc/novajoin/krb5.keytab'
|
||||||
|
self.ipaclient = IPAClient()
|
||||||
|
self.conn = openstack.connect(
|
||||||
|
auth_url='http://127.0.0.1/identity', project_name='demo',
|
||||||
|
username='demo', password='secretadmin', region_name='RegionOne',
|
||||||
|
user_domain_id='default', project_domain_id='default',
|
||||||
|
app_name='functional-tests', app_version='1.0')
|
||||||
|
self._key = self.conn.compute.create_keypair(name=TEST_KEY)
|
||||||
|
group = self.conn.network.find_security_group('default')
|
||||||
|
self._rules = []
|
||||||
|
for protocol, port in [('icmp', None), ('tcp', 22), ('tcp', 443)]:
|
||||||
|
try:
|
||||||
|
self._rules.append(
|
||||||
|
self.conn.network.create_security_group_rule(
|
||||||
|
security_group_id=group.id, direction='ingress',
|
||||||
|
remote_ip_prefix='0.0.0.0/0', protocol=protocol,
|
||||||
|
port_range_max=port, port_range_min=port,
|
||||||
|
ethertype='IPv4'))
|
||||||
|
except openstack.exceptions.ConflictException:
|
||||||
|
pass
|
||||||
|
network = self.conn.network.find_network('public')
|
||||||
|
self._ip = self.conn.network.create_ip(floating_network_id=network.id)
|
||||||
|
self._server = None
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super(TestEnrollment, self).tearDown()
|
||||||
|
self.conn.compute.delete_keypair(self._key)
|
||||||
|
for rule in self._rules:
|
||||||
|
self.conn.network.delete_security_group_rule(rule)
|
||||||
|
self._delete_server()
|
||||||
|
self.conn.network.delete_ip(self._ip)
|
||||||
|
|
||||||
|
def _create_server(self):
|
||||||
|
image = self.conn.compute.find_image(TEST_IMAGE)
|
||||||
|
flavor = self.conn.compute.find_flavor('m1.small')
|
||||||
|
network = self.conn.network.find_network('private')
|
||||||
|
|
||||||
|
self._server = self.conn.compute.create_server(
|
||||||
|
name=TEST_INSTANCE, image_id=image.id, flavor_id=flavor.id,
|
||||||
|
networks=[{"uuid": network.id}], key_name=self._key.name,
|
||||||
|
metadata = {"ipa_enroll": "True"})
|
||||||
|
|
||||||
|
server = self.conn.compute.wait_for_server(self._server)
|
||||||
|
self.conn.compute.add_floating_ip_to_server(
|
||||||
|
server, self._ip.floating_ip_address)
|
||||||
|
return server
|
||||||
|
|
||||||
|
def _delete_server(self):
|
||||||
|
if self._server:
|
||||||
|
self.conn.compute.delete_server(self._server)
|
||||||
|
self._server = None
|
||||||
|
|
||||||
|
@loopingcall.RetryDecorator(200, 5, 5, (AssertionError,))
|
||||||
|
def _check_ipa_client_created(self):
|
||||||
|
self.assertTrue(
|
||||||
|
self.ipaclient.find_host(TEST_INSTANCE + EXAMPLE_DOMAIN))
|
||||||
|
|
||||||
|
@loopingcall.RetryDecorator(50, 5, 5, (AssertionError,))
|
||||||
|
def _check_ipa_client_deleted(self):
|
||||||
|
self.assertFalse(
|
||||||
|
self.ipaclient.find_host(TEST_INSTANCE + EXAMPLE_DOMAIN))
|
||||||
|
|
||||||
|
def test_enroll_server(self):
|
||||||
|
self._create_server()
|
||||||
|
self._check_ipa_client_created()
|
||||||
|
self._delete_server()
|
||||||
|
self._check_ipa_client_deleted()
|
|
@ -18,35 +18,21 @@ import logging
|
||||||
import os
|
import os
|
||||||
import pwd
|
import pwd
|
||||||
import shutil
|
import shutil
|
||||||
import socket
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import copy
|
|
||||||
import tempfile
|
|
||||||
import getpass
|
import getpass
|
||||||
from ipapython.ipautil import run, user_input
|
from ipapython.ipautil import run, user_input
|
||||||
from ipalib import api
|
from ipalib import api
|
||||||
from ipalib import errors
|
from ipalib import errors
|
||||||
from novajoin.errors import ConfigurationError
|
from novajoin.errors import ConfigurationError
|
||||||
from novajoin import configure_ipa
|
from novajoin import configure_ipa
|
||||||
from six.moves import input
|
|
||||||
from six.moves.configparser import SafeConfigParser, NoOptionError
|
from six.moves.configparser import SafeConfigParser, NoOptionError
|
||||||
from subprocess import CalledProcessError
|
|
||||||
from string import Template
|
|
||||||
from urllib3.util import parse_url
|
from urllib3.util import parse_url
|
||||||
|
|
||||||
try:
|
|
||||||
from ipapython.ipautil import kinit_password
|
|
||||||
except ImportError:
|
|
||||||
# The import moved in freeIPA 4.5.0
|
|
||||||
from ipalib.install.kinit import kinit_password
|
|
||||||
|
|
||||||
|
|
||||||
DATADIR = '/usr/share/novajoin'
|
|
||||||
NOVADIR = '/etc/nova'
|
|
||||||
IPACONF = '/etc/ipa/default.conf'
|
IPACONF = '/etc/ipa/default.conf'
|
||||||
NOVACONF = '/etc/nova/nova.conf'
|
NOVACONF = '/etc/nova/nova.conf'
|
||||||
|
NOVACPUCONF = '/etc/nova/nova-cpu.conf'
|
||||||
JOINCONF = '/etc/novajoin/join.conf'
|
JOINCONF = '/etc/novajoin/join.conf'
|
||||||
|
|
||||||
|
|
||||||
|
@ -84,7 +70,7 @@ def openlogs():
|
||||||
def install(opts):
|
def install(opts):
|
||||||
logger.info('Installation initiated')
|
logger.info('Installation initiated')
|
||||||
|
|
||||||
if not os.path.exists(IPACONF):
|
if not os.path.exists(opts.ipa_conf):
|
||||||
raise ConfigurationError('Must be enrolled in IPA')
|
raise ConfigurationError('Must be enrolled in IPA')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -96,7 +82,7 @@ def install(opts):
|
||||||
% e.message)
|
% e.message)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = pwd.getpwnam(opts.user)
|
pwd.getpwnam(opts.user)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ConfigurationError('User: %s not found on the system' %
|
raise ConfigurationError('User: %s not found on the system' %
|
||||||
opts.user)
|
opts.user)
|
||||||
|
@ -118,7 +104,7 @@ def install(opts):
|
||||||
keystone_url = parse_url(opts.keystone_auth_url)
|
keystone_url = parse_url(opts.keystone_auth_url)
|
||||||
|
|
||||||
config = SafeConfigParser()
|
config = SafeConfigParser()
|
||||||
config.read(JOINCONF)
|
config.read(opts.novajoin_conf)
|
||||||
config.set('DEFAULT', 'domain', api.env.domain) # pylint: disable=no-member
|
config.set('DEFAULT', 'domain', api.env.domain) # pylint: disable=no-member
|
||||||
config.set('DEFAULT',
|
config.set('DEFAULT',
|
||||||
'api_paste_config',
|
'api_paste_config',
|
||||||
|
@ -152,11 +138,11 @@ def install(opts):
|
||||||
config.set('keystone_authtoken', 'user_domain_id', 'default')
|
config.set('keystone_authtoken', 'user_domain_id', 'default')
|
||||||
|
|
||||||
|
|
||||||
with open(JOINCONF, 'w') as f:
|
with open(opts.novajoin_conf, 'w') as f:
|
||||||
config.write(f)
|
config.write(f)
|
||||||
|
|
||||||
config = SafeConfigParser()
|
config = SafeConfigParser()
|
||||||
config.read(NOVACONF)
|
config.read(opts.nova_conf)
|
||||||
config.set('DEFAULT',
|
config.set('DEFAULT',
|
||||||
'vendordata_jsonfile_path',
|
'vendordata_jsonfile_path',
|
||||||
'/etc/novajoin/cloud-config-novajoin.json')
|
'/etc/novajoin/cloud-config-novajoin.json')
|
||||||
|
@ -176,15 +162,6 @@ def install(opts):
|
||||||
'vendordata_dynamic_targets',
|
'vendordata_dynamic_targets',
|
||||||
'join@http://127.0.0.1:9090/v1/')
|
'join@http://127.0.0.1:9090/v1/')
|
||||||
|
|
||||||
# Notifications
|
|
||||||
config.set('DEFAULT',
|
|
||||||
'notification_topic',
|
|
||||||
'notifications')
|
|
||||||
|
|
||||||
config.set('DEFAULT',
|
|
||||||
'notify_on_state_change',
|
|
||||||
'vm_state')
|
|
||||||
|
|
||||||
if not config.has_section('vendordata_dynamic_auth'):
|
if not config.has_section('vendordata_dynamic_auth'):
|
||||||
config.add_section('vendordata_dynamic_auth')
|
config.add_section('vendordata_dynamic_auth')
|
||||||
|
|
||||||
|
@ -201,14 +178,36 @@ def install(opts):
|
||||||
except NoOptionError:
|
except NoOptionError:
|
||||||
transport_url = None
|
transport_url = None
|
||||||
|
|
||||||
with open(NOVACONF, 'w') as f:
|
with open(opts.nova_conf, 'w') as f:
|
||||||
config.write(f)
|
config.write(f)
|
||||||
|
|
||||||
|
# Notifications
|
||||||
|
for conf in set([opts.nova_conf, opts.nova_cpu_conf]):
|
||||||
|
config = SafeConfigParser()
|
||||||
|
config.read(conf)
|
||||||
|
if not config.has_section('notifications'):
|
||||||
|
config.add_section('notifications')
|
||||||
|
|
||||||
|
config.set('notifications',
|
||||||
|
'notify_on_state_change',
|
||||||
|
'vm_state')
|
||||||
|
|
||||||
|
config.set('notifications',
|
||||||
|
'notification_format',
|
||||||
|
'unversioned')
|
||||||
|
|
||||||
|
config.set('oslo_messaging_notifications',
|
||||||
|
'topics',
|
||||||
|
'notifications,novajoin_notifications')
|
||||||
|
with open(conf, 'w') as f:
|
||||||
|
config.write(f)
|
||||||
|
|
||||||
|
|
||||||
if transport_url:
|
if transport_url:
|
||||||
join_config = SafeConfigParser()
|
join_config = SafeConfigParser()
|
||||||
join_config.read(JOINCONF)
|
join_config.read(opts.novajoin_conf)
|
||||||
join_config.set('DEFAULT', 'transport_url', transport_url)
|
join_config.set('DEFAULT', 'transport_url', transport_url)
|
||||||
with open(JOINCONF, 'w') as f:
|
with open(opts.novajoin_conf, 'w') as f:
|
||||||
join_config.write(f)
|
join_config.write(f)
|
||||||
|
|
||||||
logger.info('Importing IPA metadata')
|
logger.info('Importing IPA metadata')
|
||||||
|
@ -235,6 +234,16 @@ def parse_args():
|
||||||
help='Nova service user password', default=None)
|
help='Nova service user password', default=None)
|
||||||
parser.add_argument('--project', dest='project_name',
|
parser.add_argument('--project', dest='project_name',
|
||||||
help='Keystone project', default='service')
|
help='Keystone project', default='service')
|
||||||
|
parser.add_argument('--ipa-conf', dest='ipa_conf',
|
||||||
|
help='IPA configuration file', default=IPACONF)
|
||||||
|
parser.add_argument('--nova-conf', dest='nova_conf',
|
||||||
|
help='nova configuration file', default=NOVACONF)
|
||||||
|
parser.add_argument('--nova-cpu-conf', dest='nova_cpu_conf',
|
||||||
|
help='nova compute configuration file',
|
||||||
|
default=NOVACPUCONF)
|
||||||
|
parser.add_argument('--novajoin-conf', dest='novajoin_conf',
|
||||||
|
help='novajoin configuration file',
|
||||||
|
default=JOINCONF)
|
||||||
parser = configure_ipa.ipa_options(parser)
|
parser = configure_ipa.ipa_options(parser)
|
||||||
|
|
||||||
opts = parser.parse_args()
|
opts = parser.parse_args()
|
||||||
|
|
Loading…
Reference in New Issue