| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 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.CloudAccount() |
| 374 | """ |
| 375 | account = RwcalYang.CloudAccount() |
| 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 | """ |