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:
Grzegorz Grasza 2018-11-15 14:29:59 +01:00
parent 47ce2f7136
commit fe72231faa
4 changed files with 193 additions and 37 deletions

View File

@ -107,10 +107,16 @@ novajoin REST service and enable notifications in
vendordata_dynamic_read_timeout = 30
vendordata_jsonfile_path = /etc/novajoin/cloud-config-novajoin.json
[oslo_messaging_notifications]
notification_driver = messaging
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!
Novajoin enables keystone authentication by default, as seen in
**/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
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
notifications will be roughly split between the two services in a

View File

@ -17,6 +17,7 @@ import os
import time
import uuid
import six
from six.moves import http_client
@ -481,6 +482,14 @@ class IPAClient(IPANovaJoinBase):
result = self._call_ipa('service_find', *params, **service_args)
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):
LOG.debug('Deleting service: ' + principal)
params = [principal]

View File

@ -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()

View File

@ -18,35 +18,21 @@ import logging
import os
import pwd
import shutil
import socket
import subprocess
import sys
import time
import copy
import tempfile
import getpass
from ipapython.ipautil import run, user_input
from ipalib import api
from ipalib import errors
from novajoin.errors import ConfigurationError
from novajoin import configure_ipa
from six.moves import input
from six.moves.configparser import SafeConfigParser, NoOptionError
from subprocess import CalledProcessError
from string import Template
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'
NOVACONF = '/etc/nova/nova.conf'
NOVACPUCONF = '/etc/nova/nova-cpu.conf'
JOINCONF = '/etc/novajoin/join.conf'
@ -84,7 +70,7 @@ def openlogs():
def install(opts):
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')
try:
@ -96,7 +82,7 @@ def install(opts):
% e.message)
try:
user = pwd.getpwnam(opts.user)
pwd.getpwnam(opts.user)
except KeyError:
raise ConfigurationError('User: %s not found on the system' %
opts.user)
@ -118,7 +104,7 @@ def install(opts):
keystone_url = parse_url(opts.keystone_auth_url)
config = SafeConfigParser()
config.read(JOINCONF)
config.read(opts.novajoin_conf)
config.set('DEFAULT', 'domain', api.env.domain) # pylint: disable=no-member
config.set('DEFAULT',
'api_paste_config',
@ -152,11 +138,11 @@ def install(opts):
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 = SafeConfigParser()
config.read(NOVACONF)
config.read(opts.nova_conf)
config.set('DEFAULT',
'vendordata_jsonfile_path',
'/etc/novajoin/cloud-config-novajoin.json')
@ -176,15 +162,6 @@ def install(opts):
'vendordata_dynamic_targets',
'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'):
config.add_section('vendordata_dynamic_auth')
@ -201,14 +178,36 @@ def install(opts):
except NoOptionError:
transport_url = None
with open(NOVACONF, 'w') as f:
with open(opts.nova_conf, 'w') as 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:
join_config = SafeConfigParser()
join_config.read(JOINCONF)
join_config.read(opts.novajoin_conf)
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)
logger.info('Importing IPA metadata')
@ -235,6 +234,16 @@ def parse_args():
help='Nova service user password', default=None)
parser.add_argument('--project', dest='project_name',
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)
opts = parser.parse_args()