Browse Source

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
Grzegorz Grasza 5 months ago
parent
commit
fe72231faa
4 changed files with 193 additions and 37 deletions
  1. 20
    5
      README.rst
  2. 9
    0
      novajoin/ipa.py
  3. 123
    0
      novajoin/tests/functional/test_enrollment.py
  4. 41
    32
      scripts/novajoin-install

+ 20
- 5
README.rst View File

@@ -107,10 +107,16 @@ novajoin REST service and enable notifications in
107 107
     vendordata_dynamic_read_timeout = 30
108 108
     vendordata_jsonfile_path = /etc/novajoin/cloud-config-novajoin.json
109 109
 
110
-    [oslo_messaging_notifications]
111
-    notification_driver = messaging
112
-    notification_topic = notifications,novajoin_notifications
110
+    [notifications]
113 111
     notify_on_state_change = vm_state
112
+    notification_format = unversioned
113
+
114
+    [oslo_messaging_notifications]
115
+    ...
116
+    topics=notifications,novajoin_notifications
117
+
118
+.. note::
119
+   Notifications have to be also enabled and configured on nova computes!
114 120
 
115 121
 Novajoin enables keystone authentication by default, as seen in
116 122
 **/etc/novajoin/join-api-paste.ini**. So credentials need to be set for
@@ -203,9 +209,18 @@ Notification listener Configuration
203 209
 ===================================
204 210
 
205 211
 The only special configuration needed here is to configure nova to
206
-send notifications to the novajoin topic in /etc/nova/nova.conf:
212
+send notifications to the novajoin topic in /etc/nova/nova.conf::
207 213
 
208
-    notification_topic = notifications,novajoin_notifications
214
+    [notifications]
215
+    notify_on_state_change = vm_state
216
+    notification_format = unversioned
217
+
218
+    [oslo_messaging_notifications]
219
+    ...
220
+    topics=notifications,novajoin_notifications
221
+
222
+.. note::
223
+   Notifications have to be also enabled and configured on nova computes!
209 224
 
210 225
 If you simply use notifications and ceilometer is running then the
211 226
 notifications will be roughly split between the two services in a

+ 9
- 0
novajoin/ipa.py View File

@@ -17,6 +17,7 @@ import os
17 17
 import time
18 18
 import uuid
19 19
 
20
+import six
20 21
 from six.moves import http_client
21 22
 
22 23
 
@@ -481,6 +482,14 @@ class IPAClient(IPANovaJoinBase):
481 482
         result = self._call_ipa('service_find', *params, **service_args)
482 483
         return result['count'] > 0
483 484
 
485
+    def find_host(self, hostname):
486
+        """Return True if this host exists"""
487
+        LOG.debug('Checking if host ' + hostname + ' exists')
488
+        params = []
489
+        service_args = {'fqdn': six.text_type(hostname)}
490
+        result = self._call_ipa('host_find', *params, **service_args)
491
+        return result['count'] > 0
492
+
484 493
     def delete_service(self, principal, batch=True):
485 494
         LOG.debug('Deleting service: ' + principal)
486 495
         params = [principal]

+ 123
- 0
novajoin/tests/functional/test_enrollment.py View File

@@ -0,0 +1,123 @@
1
+# Copyright 2018 Red Hat, Inc.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+"""
16
+Tests enrollment of new OpenStack VMs in FreeIPA.
17
+
18
+The test uses the default demo project and credentials and assumes there is a
19
+centos-image present in Glance.
20
+"""
21
+
22
+import os
23
+import testtools
24
+import uuid
25
+
26
+import openstack
27
+from oslo_service import loopingcall
28
+
29
+from novajoin import config
30
+from novajoin.ipa import IPAClient
31
+
32
+
33
+CONF = config.CONF
34
+
35
+EXAMPLE_DOMAIN = '.example.test'
36
+TEST_IMAGE = 'centos-image'
37
+TEST_IMAGE_USER = 'centos'
38
+TEST_INSTANCE = str(uuid.uuid4())
39
+TEST_KEY = str(uuid.uuid4())
40
+
41
+
42
+class TestEnrollment(testtools.TestCase):
43
+    """Do a live test against a Devstack installation.
44
+
45
+    This requires:
46
+        - Devstack running on localhost
47
+        - novajoin configured and running
48
+        - centos-image present in Glance
49
+
50
+    This will add and remove server instances.
51
+    """
52
+
53
+    def setUp(self):
54
+        super(TestEnrollment, self).setUp()
55
+        CONF.keytab = '/tmp/test.keytab'
56
+        if not os.path.isfile(CONF.keytab):
57
+            CONF.keytab = '/etc/novajoin/krb5.keytab'
58
+        self.ipaclient = IPAClient()
59
+        self.conn = openstack.connect(
60
+            auth_url='http://127.0.0.1/identity', project_name='demo',
61
+            username='demo', password='secretadmin', region_name='RegionOne',
62
+            user_domain_id='default', project_domain_id='default',
63
+            app_name='functional-tests', app_version='1.0')
64
+        self._key = self.conn.compute.create_keypair(name=TEST_KEY)
65
+        group = self.conn.network.find_security_group('default')
66
+        self._rules = []
67
+        for protocol, port in [('icmp', None), ('tcp', 22), ('tcp', 443)]:
68
+            try:
69
+                self._rules.append(
70
+                    self.conn.network.create_security_group_rule(
71
+                        security_group_id=group.id, direction='ingress',
72
+                        remote_ip_prefix='0.0.0.0/0', protocol=protocol,
73
+                        port_range_max=port, port_range_min=port,
74
+                        ethertype='IPv4'))
75
+            except openstack.exceptions.ConflictException:
76
+                pass
77
+        network = self.conn.network.find_network('public')
78
+        self._ip = self.conn.network.create_ip(floating_network_id=network.id)
79
+        self._server = None
80
+
81
+    def tearDown(self):
82
+        super(TestEnrollment, self).tearDown()
83
+        self.conn.compute.delete_keypair(self._key)
84
+        for rule in self._rules:
85
+            self.conn.network.delete_security_group_rule(rule)
86
+        self._delete_server()
87
+        self.conn.network.delete_ip(self._ip)
88
+
89
+    def _create_server(self):
90
+        image = self.conn.compute.find_image(TEST_IMAGE)
91
+        flavor = self.conn.compute.find_flavor('m1.small')
92
+        network = self.conn.network.find_network('private')
93
+
94
+        self._server = self.conn.compute.create_server(
95
+            name=TEST_INSTANCE, image_id=image.id, flavor_id=flavor.id,
96
+            networks=[{"uuid": network.id}], key_name=self._key.name,
97
+            metadata = {"ipa_enroll": "True"})
98
+
99
+        server = self.conn.compute.wait_for_server(self._server)
100
+        self.conn.compute.add_floating_ip_to_server(
101
+            server, self._ip.floating_ip_address)
102
+        return server
103
+
104
+    def _delete_server(self):
105
+        if self._server:
106
+            self.conn.compute.delete_server(self._server)
107
+        self._server = None
108
+
109
+    @loopingcall.RetryDecorator(200, 5, 5, (AssertionError,))
110
+    def _check_ipa_client_created(self):
111
+        self.assertTrue(
112
+            self.ipaclient.find_host(TEST_INSTANCE + EXAMPLE_DOMAIN))
113
+
114
+    @loopingcall.RetryDecorator(50, 5, 5, (AssertionError,))
115
+    def _check_ipa_client_deleted(self):
116
+        self.assertFalse(
117
+            self.ipaclient.find_host(TEST_INSTANCE + EXAMPLE_DOMAIN))
118
+
119
+    def test_enroll_server(self):
120
+        self._create_server()
121
+        self._check_ipa_client_created()
122
+        self._delete_server()
123
+        self._check_ipa_client_deleted()

+ 41
- 32
scripts/novajoin-install View File

@@ -18,35 +18,21 @@ import logging
18 18
 import os
19 19
 import pwd
20 20
 import shutil
21
-import socket
22
-import subprocess
23 21
 import sys
24 22
 import time
25
-import copy
26
-import tempfile
27 23
 import getpass
28 24
 from ipapython.ipautil import run, user_input
29 25
 from ipalib import api
30 26
 from ipalib import errors
31 27
 from novajoin.errors import ConfigurationError
32 28
 from novajoin import configure_ipa
33
-from six.moves import input
34 29
 from six.moves.configparser import SafeConfigParser, NoOptionError
35
-from subprocess import CalledProcessError
36
-from string import Template
37 30
 from urllib3.util import parse_url
38 31
 
39
-try:
40
-    from ipapython.ipautil import kinit_password
41
-except ImportError:
42
-    # The import moved in freeIPA 4.5.0
43
-    from ipalib.install.kinit import kinit_password
44 32
 
45
-
46
-DATADIR = '/usr/share/novajoin'
47
-NOVADIR = '/etc/nova'
48 33
 IPACONF = '/etc/ipa/default.conf'
49 34
 NOVACONF = '/etc/nova/nova.conf'
35
+NOVACPUCONF = '/etc/nova/nova-cpu.conf'
50 36
 JOINCONF = '/etc/novajoin/join.conf'
51 37
 
52 38
 
@@ -84,7 +70,7 @@ def openlogs():
84 70
 def install(opts):
85 71
     logger.info('Installation initiated')
86 72
 
87
-    if not os.path.exists(IPACONF):
73
+    if not os.path.exists(opts.ipa_conf):
88 74
         raise ConfigurationError('Must be enrolled in IPA')
89 75
 
90 76
     try:
@@ -96,7 +82,7 @@ def install(opts):
96 82
                                  % e.message)
97 83
 
98 84
     try:
99
-        user = pwd.getpwnam(opts.user)
85
+        pwd.getpwnam(opts.user)
100 86
     except KeyError:
101 87
         raise ConfigurationError('User: %s not found on the system' %
102 88
                                  opts.user)
@@ -118,7 +104,7 @@ def install(opts):
118 104
     keystone_url = parse_url(opts.keystone_auth_url)
119 105
 
120 106
     config = SafeConfigParser()
121
-    config.read(JOINCONF)
107
+    config.read(opts.novajoin_conf)
122 108
     config.set('DEFAULT', 'domain', api.env.domain)  # pylint: disable=no-member
123 109
     config.set('DEFAULT',
124 110
                'api_paste_config',
@@ -152,11 +138,11 @@ def install(opts):
152 138
     config.set('keystone_authtoken', 'user_domain_id', 'default')
153 139
 
154 140
 
155
-    with open(JOINCONF, 'w') as f:
141
+    with open(opts.novajoin_conf, 'w') as f:
156 142
         config.write(f)
157 143
 
158 144
     config = SafeConfigParser()
159
-    config.read(NOVACONF)
145
+    config.read(opts.nova_conf)
160 146
     config.set('DEFAULT',
161 147
                'vendordata_jsonfile_path',
162 148
                '/etc/novajoin/cloud-config-novajoin.json')
@@ -176,15 +162,6 @@ def install(opts):
176 162
                'vendordata_dynamic_targets',
177 163
                'join@http://127.0.0.1:9090/v1/')
178 164
 
179
-    # Notifications
180
-    config.set('DEFAULT',
181
-               'notification_topic',
182
-               'notifications')
183
-
184
-    config.set('DEFAULT',
185
-               'notify_on_state_change',
186
-               'vm_state')
187
-
188 165
     if not config.has_section('vendordata_dynamic_auth'):
189 166
         config.add_section('vendordata_dynamic_auth')
190 167
 
@@ -201,14 +178,36 @@ def install(opts):
201 178
     except NoOptionError:
202 179
         transport_url = None
203 180
 
204
-    with open(NOVACONF, 'w') as f:
181
+    with open(opts.nova_conf, 'w') as f:
205 182
         config.write(f)
206 183
 
184
+    # Notifications
185
+    for conf in set([opts.nova_conf, opts.nova_cpu_conf]):
186
+        config = SafeConfigParser()
187
+        config.read(conf)
188
+        if not config.has_section('notifications'):
189
+            config.add_section('notifications')
190
+
191
+        config.set('notifications',
192
+                   'notify_on_state_change',
193
+                   'vm_state')
194
+
195
+        config.set('notifications',
196
+                   'notification_format',
197
+                   'unversioned')
198
+
199
+        config.set('oslo_messaging_notifications',
200
+                   'topics',
201
+                   'notifications,novajoin_notifications')
202
+        with open(conf, 'w') as f:
203
+            config.write(f)
204
+
205
+
207 206
     if transport_url:
208 207
         join_config = SafeConfigParser()
209
-        join_config.read(JOINCONF)
208
+        join_config.read(opts.novajoin_conf)
210 209
         join_config.set('DEFAULT', 'transport_url', transport_url)
211
-        with open(JOINCONF, 'w') as f:
210
+        with open(opts.novajoin_conf, 'w') as f:
212 211
             join_config.write(f)
213 212
 
214 213
     logger.info('Importing IPA metadata')
@@ -235,6 +234,16 @@ def parse_args():
235 234
                         help='Nova service user password', default=None)
236 235
     parser.add_argument('--project', dest='project_name',
237 236
                         help='Keystone project', default='service')
237
+    parser.add_argument('--ipa-conf', dest='ipa_conf',
238
+                        help='IPA configuration file', default=IPACONF)
239
+    parser.add_argument('--nova-conf', dest='nova_conf',
240
+                        help='nova configuration file', default=NOVACONF)
241
+    parser.add_argument('--nova-cpu-conf', dest='nova_cpu_conf',
242
+                        help='nova compute configuration file',
243
+                        default=NOVACPUCONF)
244
+    parser.add_argument('--novajoin-conf', dest='novajoin_conf',
245
+                        help='novajoin configuration file',
246
+                        default=JOINCONF)
238 247
     parser = configure_ipa.ipa_options(parser)
239 248
 
240 249
     opts = parser.parse_args()

Loading…
Cancel
Save