update from RIFT as of 696b75d2fe9fb046261b08c616f1bcf6c0b54a9b second try
[osm/SO.git] / rwcal / test / test_openstack_install.py
1 """
2 #
3 #
4 # Copyright 2016 RIFT.IO Inc
5 #
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
9 #
10 # http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
17 #
18 #
19 # @file test_openstack_install.py
20 # @author Varun Prasad (varun.prasad@riftio.com)
21 # @date 10/10/2015
22 # @brief Test Openstack/os install
23 #
24 """
25
26 import logging
27 import re
28 import socket
29 import sys
30 import time
31 import tempfile
32
33 from keystoneclient.v3 import client
34 import paramiko
35 import pytest
36 import requests
37 import xmlrpc.client
38
39 from gi.repository import RwcalYang
40 from gi.repository.RwTypes import RwStatus
41 import rw_peas
42 import rwlogger
43
44
45 logger = logging.getLogger()
46 logging.basicConfig(level=logging.INFO)
47
48
49 class Host(object):
50 """A wrapper on top of a host, which provides a ssh connection instance.
51
52 Assumption:
53 The username/password for the VM is default.
54 """
55 _USERNAME = "root"
56 _PASSWORD = "riftIO"
57
58 def __init__(self, hostname):
59 """
60 Args:
61 hostname (str): Hostname (grunt3.qanet.riftio.com)
62 """
63 self.hostname = hostname
64 try:
65 self.ip = socket.gethostbyname(hostname)
66 except socket.gaierror:
67 logger.error("Unable to resolve the hostname {}".format(hostname))
68 sys.exit(1)
69
70 self.ssh = paramiko.SSHClient()
71 # Note: Do not load the system keys as the test will fail if the keys
72 # change.
73 self.ssh.set_missing_host_key_policy(paramiko.WarningPolicy())
74
75 def connect(self):
76 """Set up ssh connection.
77 """
78 logger.debug("Trying to connect to {}: {}".format(
79 self.hostname,
80 self.ip))
81
82 self.ssh.connect(
83 self.ip,
84 username=self._USERNAME,
85 password=self._PASSWORD)
86
87 def put(self, content, dest):
88 """Creates a tempfile and puts it in the destination path in the HOST.
89 Args:
90 content (str): Content to be written to a file.
91 dest (str): Path to store the content.
92 """
93 temp_file = tempfile.NamedTemporaryFile(delete=False)
94 temp_file.write(content.encode("UTF-8"))
95 temp_file.close()
96
97 logger.info("Writing {} file in {}".format(dest, self.hostname))
98 sftp = self.ssh.open_sftp()
99 sftp.put(temp_file.name, dest)
100 sftp.close()
101
102 def clear(self):
103 """Clean up
104 """
105 self.ssh.close()
106
107
108 class Grunt(Host):
109 """A wrapper on top of grunt machine, provides functionalities to check
110 if the grunt is up, IP resolution.
111 """
112 @property
113 def grunt_name(self):
114 """Extract the grunt name from the FQDN
115
116 Returns:
117 str: e.g. grunt3 from grunt3.qanet.riftio.com
118 """
119 return self.hostname.split(".")[0]
120
121 @property
122 def dns_server(self):
123 """Hard-coded for now.
124 """
125 return "10.95.0.3"
126
127 @property
128 def floating_ip(self):
129 return "10.95.1.0"
130
131 @property
132 def private_ip(self):
133 """Construct the private IP from the grunt name. 10.0.xx.0 where xx is
134 value of the grunt (3 in case of grunt3)
135 """
136 host_part = re.sub(r"[a-zA-z]+", "", self.grunt_name)
137 return '10.0.{}.0'.format(host_part)
138
139 def is_system_up(self):
140 """Checks if system is up using ssh login.
141
142 Returns:
143 bool: Indicates if system is UP
144 """
145 try:
146 self.connect()
147 except OSError:
148 return False
149
150 return True
151
152 def wait_till_system_is_up(self, timeout=50, check_openstack=False):
153 """Blocking call to check if system is up.
154 Args:
155 timeout (int, optional): In mins(~).
156 check_openstack (bool, optional): If true will also check if
157 openstack is up and running on the system.
158
159 Raises:
160 OSError: If system start exceeds the timeout
161 """
162
163 TRY_DURATION = 20 # secs
164 total_tries = timeout * (60 / TRY_DURATION) # 3 tries/mins i.e. 20 secs.
165 tries = 0
166
167 while tries < total_tries:
168 if self.is_system_up():
169 if check_openstack and self.is_openstack_up():
170 return
171 elif not check_openstack:
172 return
173
174 logger.info("{} down: Sleeping for {} secs. Try {} of {}".format(
175 self.hostname,
176 TRY_DURATION,
177 tries,
178 int(total_tries)))
179
180 time.sleep(TRY_DURATION)
181 tries += 1
182
183 raise OSError("Exception in system start {}({})".format(
184 self.hostname,
185 self.ip))
186
187 def is_openstack_up(self):
188 """Checks if openstack is UP, by verifying the URL.
189
190 Returns:
191 bool: Indicates if system is UP
192 """
193 url = "http://{}/dashboard/".format(self.ip)
194
195 logger.info("Checking if openstack({}) is UP".format(url))
196
197 try:
198 requests.get(url)
199 except requests.ConnectionError:
200 return False
201
202 return True
203
204
205 class Cobbler(Host):
206 """A thin wrapper on cobbler and provides an interface using XML rpc client.
207
208 Assumption:
209 System instances are already added to cobbler(with ipmi). Adding instances
210 can also be automated, can be taken up sometime later.
211 """
212 def __init__(self, hostname, username="cobbler", password="cobbler"):
213 """
214 Args:
215 hostname (str): Cobbler host.
216 username (str, optional): username.
217 password (str, optional): password
218 """
219 super().__init__(hostname)
220
221 url = "https://{}/cobbler_api".format(hostname)
222
223 self.server = xmlrpc.client.ServerProxy(url)
224 logger.info("obtained a cobbler instance for the host {}".format(hostname))
225
226 self.token = self.server.login(username, password)
227 self.connect()
228
229 def create_profile(self, profile_name, ks_file):
230 """Create the profile for the system.
231
232 Args:
233 profile_name (str): Name of the profile.
234 ks_file (str): Path of the kick start file.
235 """
236 profile_attrs = {
237 "name": profile_name,
238 "kickstart": ks_file,
239 "repos": ['riftware', 'rift-misc', 'fc21-x86_64-updates',
240 'fc21-x86_64', 'openstack-kilo'],
241 "owners": ["admin"],
242 "distro": "FC21.3-x86_64"
243 }
244
245 profile_id = self.server.new_profile(self.token)
246 for key, value in profile_attrs.items():
247 self.server.modify_profile(profile_id, key, value, self.token)
248 self.server.save_profile(profile_id, self.token)
249
250 def create_snippet(self, snippet_name, snippet_content):
251 """Unfortunately the XML rpc apis don't provide a direct interface to
252 create snippets, so falling back on the default sftp methods.
253
254 Args:
255 snippet_name (str): Name.
256 snippet_content (str): snippet's content.
257
258 Returns:
259 str: path where the snippet is stored
260 """
261 path = "/var/lib/cobbler/snippets/{}".format(snippet_name)
262 self.put(snippet_content, path)
263 return path
264
265 def create_kickstart(self, ks_name, ks_content):
266 """Creates and returns the path of the ks file.
267
268 Args:
269 ks_name (str): Name of the ks file to be saved.
270 ks_content (str): Content for ks file.
271
272 Returns:
273 str: path where the ks file is saved.
274 """
275 path = "/var/lib/cobbler/kickstarts/{}".format(ks_name)
276 self.put(ks_content, path)
277 return path
278
279 def boot_system(self, grunt, profile_name, false_boot=False):
280 """Boots the system with the profile specified. Also enable net-boot
281
282 Args:
283 grunt (Grunt): instance of grunt
284 profile_name (str): A valid profile name.
285 false_boot (bool, optional): debug only option.
286 """
287 if false_boot:
288 return
289
290 system_id = self.server.get_system_handle(
291 grunt.grunt_name,
292 self.token)
293 self.server.modify_system(
294 system_id,
295 "profile",
296 profile_name,
297 self.token)
298
299 self.server.modify_system(
300 system_id,
301 "netboot_enabled",
302 "True",
303 self.token)
304 self.server.save_system(system_id, self.token)
305 self.server.power_system(system_id, "reboot", self.token)
306
307
308 class OpenstackTest(object):
309 """Driver class to automate the installation.
310 """
311 def __init__(
312 self,
313 cobbler,
314 controller,
315 compute_nodes=None,
316 test_prefix="openstack_test"):
317 """
318 Args:
319 cobbler (Cobbler): Instance of Cobbler
320 controller (Controller): Controller node instance
321 compute_nodes (TYPE, optional): A list of Grunt nodes to be set up
322 as compute nodes.
323 test_prefix (str, optional): All entities created by the script are
324 prefixed with this string.
325 """
326 self.cobbler = cobbler
327 self.controller = controller
328 self.compute_nodes = [] if compute_nodes is None else compute_nodes
329 self.test_prefix = test_prefix
330
331 def _prepare_snippet(self):
332 """Prepares the config based on the controller and compute nodes.
333
334 Returns:
335 str: Openstack config content.
336 """
337 content = ""
338
339 config = {}
340 config['host_name'] = self.controller.grunt_name
341 config['ip'] = self.controller.ip
342 config['dns_server'] = self.controller.dns_server
343 config['private_ip'] = self.controller.private_ip
344 config['floating_ip'] = self.controller.floating_ip
345
346 content += Template.GRUNT_CONFIG.format(**config)
347 for compute_node in self.compute_nodes:
348 config["host_name"] = compute_node.grunt_name
349 content += Template.GRUNT_CONFIG.format(**config)
350
351 content = Template.SNIPPET_TEMPLATE.format(config=content)
352
353 return content
354
355 def prepare_profile(self):
356 """Creates the cobbler profile.
357 """
358 snippet_content = self._prepare_snippet()
359 self.cobbler.create_snippet(
360 "{}.cfg".format(self.test_prefix),
361 snippet_content)
362
363 ks_content = Template.KS_TEMPATE
364 ks_file = self.cobbler.create_kickstart(
365 "{}.ks".format(self.test_prefix),
366 ks_content)
367
368 self.cobbler.create_profile(self.test_prefix, ks_file)
369 return self.test_prefix
370
371 def _get_cal_account(self):
372 """
373 Creates an object for class RwcalYang.YangData_RwProject_Project_CloudAccounts_CloudAccountList()
374 """
375 account = RwcalYang.YangData_RwProject_Project_CloudAccounts_CloudAccountList()
376 account.account_type = "openstack"
377 account.openstack.key = "{}_user".format(self.test_prefix)
378 account.openstack.secret = "mypasswd"
379 account.openstack.auth_url = 'http://{}:35357/v3/'.format(self.controller.ip)
380 account.openstack.tenant = self.test_prefix
381
382 return account
383
384 def start(self):
385 """Starts the installation.
386 """
387 profile_name = self.prepare_profile()
388
389 self.cobbler.boot_system(self.controller, profile_name)
390 self.controller.wait_till_system_is_up(check_openstack=True)
391
392 try:
393 logger.info("Controller system is UP. Setting up compute nodes")
394 for compute_node in self.compute_nodes:
395 self.cobbler.boot_system(compute_node, profile_name)
396 compute_node.wait_till_system_is_up()
397 except OSError as e:
398 logger.error("System set-up failed {}".format(e))
399 sys.exit(1)
400
401 # Currently we don't have wrapper on top of users/projects so using
402 # keystone API directly
403 acct = self._get_cal_account()
404
405 keystone_conn = client.Client(
406 auth_url=acct.openstack.auth_url,
407 username='admin',
408 password='mypasswd')
409
410 # Create a test project
411 project = keystone_conn.projects.create(
412 acct.openstack.tenant,
413 "default",
414 description="Openstack test project")
415
416 # Create an user
417 user = keystone_conn.users.create(
418 acct.openstack.key,
419 password=acct.openstack.secret,
420 default_project=project)
421
422 # Make the newly created user as ADMIN
423 admin_role = keystone_conn.roles.list(name="admin")[0]
424 keystone_conn.roles.grant(
425 admin_role.id,
426 user=user.id,
427 project=project.id)
428
429 # nova API needs to be restarted, otherwise the new service doesn't play
430 # well
431 self.controller.ssh.exec_command("source keystonerc_admin && "
432 "service openstack-nova-api restart")
433 time.sleep(10)
434
435 return acct
436
437 def clear(self):
438 """Close out all SFTP connections.
439 """
440 nodes = [self.controller]
441 nodes.extend(self.compute_nodes)
442 for node in nodes:
443 node.clear()
444
445
446 ###############################################################################
447 ## Begin pytests
448 ###############################################################################
449
450
451 @pytest.fixture(scope="session")
452 def cal(request):
453 """
454 Loads rw.cal plugin via libpeas
455 """
456 plugin = rw_peas.PeasPlugin('rwcal_openstack', 'RwCal-1.0')
457 engine, info, extension = plugin()
458
459 # Get the RwLogger context
460 rwloggerctx = rwlogger.RwLog.Ctx.new("Cal-Log")
461
462 cal = plugin.get_interface("Cloud")
463 try:
464 rc = cal.init(rwloggerctx)
465 assert rc == RwStatus.SUCCESS
466 except:
467 logger.error("ERROR:Cal plugin instantiation failed. Aborting tests")
468 else:
469 logger.info("Openstack Cal plugin successfully instantiated")
470
471 return cal
472
473
474 @pytest.fixture(scope="session")
475 def account(request):
476 """Creates an openstack instance with 1 compute node and returns the newly
477 created account.
478 """
479 cobbler = Cobbler("qacobbler.eng.riftio.com")
480 controller = Grunt("grunt3.qanet.riftio.com")
481 compute_nodes = [Grunt("grunt5.qanet.riftio.com")]
482
483 test = OpenstackTest(cobbler, controller, compute_nodes)
484 account = test.start()
485
486 request.addfinalizer(test.clear)
487 return account
488
489
490 def test_list_images(cal, account):
491 """Verify if 2 images are present
492 """
493 status, resources = cal.get_image_list(account)
494 assert len(resources.imageinfo_list) == 2
495
496 def test_list_flavors(cal, account):
497 """Basic flavor checks
498 """
499 status, resources = cal.get_flavor_list(account)
500 assert len(resources.flavorinfo_list) == 5
501
502
503 class Template(object):
504 """A container to hold all cobbler related templates.
505 """
506 GRUNT_CONFIG = """
507 {host_name})
508 CONTROLLER={ip}
509 BRGIF=1
510 OVSDPDK=N
511 TRUSTED=N
512 QAT=N
513 HUGEPAGE=0
514 VLAN=10:14
515 PRIVATE_IP={private_ip}
516 FLOATING_IP={floating_ip}
517 DNS_SERVER={dns_server}
518 ;;
519
520 """
521
522 SNIPPET_TEMPLATE = """
523 # =====================Begining of snippet=================
524 # snippet openstack_test.cfg
525 case $name in
526
527 {config}
528
529 *)
530 ;;
531 esac
532
533 # =====================End of snippet=================
534
535 """
536
537 KS_TEMPATE = """
538 $SNIPPET('rift-repos')
539 $SNIPPET('rift-base')
540 %packages
541 @core
542 wget
543 $SNIPPET('rift-grunt-fc21-packages')
544 ganglia-gmetad
545 ganglia-gmond
546 %end
547
548 %pre
549 $SNIPPET('log_ks_pre')
550 $SNIPPET('kickstart_start')
551 # Enable installation monitoring
552 $SNIPPET('pre_anamon')
553 %end
554
555 %post --log=/root/ks_post.log
556 $SNIPPET('openstack_test.cfg')
557 $SNIPPET('ganglia')
558 $SNIPPET('rift-post-yum')
559 $SNIPPET('rift-post')
560 $SNIPPET('rift_fix_grub')
561
562 $SNIPPET('rdo-post')
563 echo "banner RDO test" >> /etc/profile
564
565 $SNIPPET('kickstart_done')
566 %end
567 """