| """ |
| # |
| # |
| # Copyright 2016 RIFT.IO 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. |
| # |
| # |
| # @file test_openstack_install.py |
| # @author Varun Prasad (varun.prasad@riftio.com) |
| # @date 10/10/2015 |
| # @brief Test Openstack/os install |
| # |
| """ |
| |
| import logging |
| import re |
| import socket |
| import sys |
| import time |
| import tempfile |
| |
| from keystoneclient.v3 import client |
| import paramiko |
| import pytest |
| import requests |
| import xmlrpc.client |
| |
| from gi.repository import RwcalYang |
| from gi.repository.RwTypes import RwStatus |
| import rw_peas |
| import rwlogger |
| |
| |
| logger = logging.getLogger() |
| logging.basicConfig(level=logging.INFO) |
| |
| |
| class Host(object): |
| """A wrapper on top of a host, which provides a ssh connection instance. |
| |
| Assumption: |
| The username/password for the VM is default. |
| """ |
| _USERNAME = "root" |
| _PASSWORD = "riftIO" |
| |
| def __init__(self, hostname): |
| """ |
| Args: |
| hostname (str): Hostname (grunt3.qanet.riftio.com) |
| """ |
| self.hostname = hostname |
| try: |
| self.ip = socket.gethostbyname(hostname) |
| except socket.gaierror: |
| logger.error("Unable to resolve the hostname {}".format(hostname)) |
| sys.exit(1) |
| |
| self.ssh = paramiko.SSHClient() |
| # Note: Do not load the system keys as the test will fail if the keys |
| # change. |
| self.ssh.set_missing_host_key_policy(paramiko.WarningPolicy()) |
| |
| def connect(self): |
| """Set up ssh connection. |
| """ |
| logger.debug("Trying to connect to {}: {}".format( |
| self.hostname, |
| self.ip)) |
| |
| self.ssh.connect( |
| self.ip, |
| username=self._USERNAME, |
| password=self._PASSWORD) |
| |
| def put(self, content, dest): |
| """Creates a tempfile and puts it in the destination path in the HOST. |
| Args: |
| content (str): Content to be written to a file. |
| dest (str): Path to store the content. |
| """ |
| temp_file = tempfile.NamedTemporaryFile(delete=False) |
| temp_file.write(content.encode("UTF-8")) |
| temp_file.close() |
| |
| logger.info("Writing {} file in {}".format(dest, self.hostname)) |
| sftp = self.ssh.open_sftp() |
| sftp.put(temp_file.name, dest) |
| sftp.close() |
| |
| def clear(self): |
| """Clean up |
| """ |
| self.ssh.close() |
| |
| |
| class Grunt(Host): |
| """A wrapper on top of grunt machine, provides functionalities to check |
| if the grunt is up, IP resolution. |
| """ |
| @property |
| def grunt_name(self): |
| """Extract the grunt name from the FQDN |
| |
| Returns: |
| str: e.g. grunt3 from grunt3.qanet.riftio.com |
| """ |
| return self.hostname.split(".")[0] |
| |
| @property |
| def dns_server(self): |
| """Hard-coded for now. |
| """ |
| return "10.95.0.3" |
| |
| @property |
| def floating_ip(self): |
| return "10.95.1.0" |
| |
| @property |
| def private_ip(self): |
| """Construct the private IP from the grunt name. 10.0.xx.0 where xx is |
| value of the grunt (3 in case of grunt3) |
| """ |
| host_part = re.sub(r"[a-zA-z]+", "", self.grunt_name) |
| return '10.0.{}.0'.format(host_part) |
| |
| def is_system_up(self): |
| """Checks if system is up using ssh login. |
| |
| Returns: |
| bool: Indicates if system is UP |
| """ |
| try: |
| self.connect() |
| except OSError: |
| return False |
| |
| return True |
| |
| def wait_till_system_is_up(self, timeout=50, check_openstack=False): |
| """Blocking call to check if system is up. |
| Args: |
| timeout (int, optional): In mins(~). |
| check_openstack (bool, optional): If true will also check if |
| openstack is up and running on the system. |
| |
| Raises: |
| OSError: If system start exceeds the timeout |
| """ |
| |
| TRY_DURATION = 20 # secs |
| total_tries = timeout * (60 / TRY_DURATION) # 3 tries/mins i.e. 20 secs. |
| tries = 0 |
| |
| while tries < total_tries: |
| if self.is_system_up(): |
| if check_openstack and self.is_openstack_up(): |
| return |
| elif not check_openstack: |
| return |
| |
| logger.info("{} down: Sleeping for {} secs. Try {} of {}".format( |
| self.hostname, |
| TRY_DURATION, |
| tries, |
| int(total_tries))) |
| |
| time.sleep(TRY_DURATION) |
| tries += 1 |
| |
| raise OSError("Exception in system start {}({})".format( |
| self.hostname, |
| self.ip)) |
| |
| def is_openstack_up(self): |
| """Checks if openstack is UP, by verifying the URL. |
| |
| Returns: |
| bool: Indicates if system is UP |
| """ |
| url = "http://{}/dashboard/".format(self.ip) |
| |
| logger.info("Checking if openstack({}) is UP".format(url)) |
| |
| try: |
| requests.get(url) |
| except requests.ConnectionError: |
| return False |
| |
| return True |
| |
| |
| class Cobbler(Host): |
| """A thin wrapper on cobbler and provides an interface using XML rpc client. |
| |
| Assumption: |
| System instances are already added to cobbler(with ipmi). Adding instances |
| can also be automated, can be taken up sometime later. |
| """ |
| def __init__(self, hostname, username="cobbler", password="cobbler"): |
| """ |
| Args: |
| hostname (str): Cobbler host. |
| username (str, optional): username. |
| password (str, optional): password |
| """ |
| super().__init__(hostname) |
| |
| url = "https://{}/cobbler_api".format(hostname) |
| |
| self.server = xmlrpc.client.ServerProxy(url) |
| logger.info("obtained a cobbler instance for the host {}".format(hostname)) |
| |
| self.token = self.server.login(username, password) |
| self.connect() |
| |
| def create_profile(self, profile_name, ks_file): |
| """Create the profile for the system. |
| |
| Args: |
| profile_name (str): Name of the profile. |
| ks_file (str): Path of the kick start file. |
| """ |
| profile_attrs = { |
| "name": profile_name, |
| "kickstart": ks_file, |
| "repos": ['riftware', 'rift-misc', 'fc21-x86_64-updates', |
| 'fc21-x86_64', 'openstack-kilo'], |
| "owners": ["admin"], |
| "distro": "FC21.3-x86_64" |
| } |
| |
| profile_id = self.server.new_profile(self.token) |
| for key, value in profile_attrs.items(): |
| self.server.modify_profile(profile_id, key, value, self.token) |
| self.server.save_profile(profile_id, self.token) |
| |
| def create_snippet(self, snippet_name, snippet_content): |
| """Unfortunately the XML rpc apis don't provide a direct interface to |
| create snippets, so falling back on the default sftp methods. |
| |
| Args: |
| snippet_name (str): Name. |
| snippet_content (str): snippet's content. |
| |
| Returns: |
| str: path where the snippet is stored |
| """ |
| path = "/var/lib/cobbler/snippets/{}".format(snippet_name) |
| self.put(snippet_content, path) |
| return path |
| |
| def create_kickstart(self, ks_name, ks_content): |
| """Creates and returns the path of the ks file. |
| |
| Args: |
| ks_name (str): Name of the ks file to be saved. |
| ks_content (str): Content for ks file. |
| |
| Returns: |
| str: path where the ks file is saved. |
| """ |
| path = "/var/lib/cobbler/kickstarts/{}".format(ks_name) |
| self.put(ks_content, path) |
| return path |
| |
| def boot_system(self, grunt, profile_name, false_boot=False): |
| """Boots the system with the profile specified. Also enable net-boot |
| |
| Args: |
| grunt (Grunt): instance of grunt |
| profile_name (str): A valid profile name. |
| false_boot (bool, optional): debug only option. |
| """ |
| if false_boot: |
| return |
| |
| system_id = self.server.get_system_handle( |
| grunt.grunt_name, |
| self.token) |
| self.server.modify_system( |
| system_id, |
| "profile", |
| profile_name, |
| self.token) |
| |
| self.server.modify_system( |
| system_id, |
| "netboot_enabled", |
| "True", |
| self.token) |
| self.server.save_system(system_id, self.token) |
| self.server.power_system(system_id, "reboot", self.token) |
| |
| |
| class OpenstackTest(object): |
| """Driver class to automate the installation. |
| """ |
| def __init__( |
| self, |
| cobbler, |
| controller, |
| compute_nodes=None, |
| test_prefix="openstack_test"): |
| """ |
| Args: |
| cobbler (Cobbler): Instance of Cobbler |
| controller (Controller): Controller node instance |
| compute_nodes (TYPE, optional): A list of Grunt nodes to be set up |
| as compute nodes. |
| test_prefix (str, optional): All entities created by the script are |
| prefixed with this string. |
| """ |
| self.cobbler = cobbler |
| self.controller = controller |
| self.compute_nodes = [] if compute_nodes is None else compute_nodes |
| self.test_prefix = test_prefix |
| |
| def _prepare_snippet(self): |
| """Prepares the config based on the controller and compute nodes. |
| |
| Returns: |
| str: Openstack config content. |
| """ |
| content = "" |
| |
| config = {} |
| config['host_name'] = self.controller.grunt_name |
| config['ip'] = self.controller.ip |
| config['dns_server'] = self.controller.dns_server |
| config['private_ip'] = self.controller.private_ip |
| config['floating_ip'] = self.controller.floating_ip |
| |
| content += Template.GRUNT_CONFIG.format(**config) |
| for compute_node in self.compute_nodes: |
| config["host_name"] = compute_node.grunt_name |
| content += Template.GRUNT_CONFIG.format(**config) |
| |
| content = Template.SNIPPET_TEMPLATE.format(config=content) |
| |
| return content |
| |
| def prepare_profile(self): |
| """Creates the cobbler profile. |
| """ |
| snippet_content = self._prepare_snippet() |
| self.cobbler.create_snippet( |
| "{}.cfg".format(self.test_prefix), |
| snippet_content) |
| |
| ks_content = Template.KS_TEMPATE |
| ks_file = self.cobbler.create_kickstart( |
| "{}.ks".format(self.test_prefix), |
| ks_content) |
| |
| self.cobbler.create_profile(self.test_prefix, ks_file) |
| return self.test_prefix |
| |
| def _get_cal_account(self): |
| """ |
| Creates an object for class RwcalYang.CloudAccount() |
| """ |
| account = RwcalYang.CloudAccount() |
| account.account_type = "openstack" |
| account.openstack.key = "{}_user".format(self.test_prefix) |
| account.openstack.secret = "mypasswd" |
| account.openstack.auth_url = 'http://{}:35357/v3/'.format(self.controller.ip) |
| account.openstack.tenant = self.test_prefix |
| |
| return account |
| |
| def start(self): |
| """Starts the installation. |
| """ |
| profile_name = self.prepare_profile() |
| |
| self.cobbler.boot_system(self.controller, profile_name) |
| self.controller.wait_till_system_is_up(check_openstack=True) |
| |
| try: |
| logger.info("Controller system is UP. Setting up compute nodes") |
| for compute_node in self.compute_nodes: |
| self.cobbler.boot_system(compute_node, profile_name) |
| compute_node.wait_till_system_is_up() |
| except OSError as e: |
| logger.error("System set-up failed {}".format(e)) |
| sys.exit(1) |
| |
| # Currently we don't have wrapper on top of users/projects so using |
| # keystone API directly |
| acct = self._get_cal_account() |
| |
| keystone_conn = client.Client( |
| auth_url=acct.openstack.auth_url, |
| username='admin', |
| password='mypasswd') |
| |
| # Create a test project |
| project = keystone_conn.projects.create( |
| acct.openstack.tenant, |
| "default", |
| description="Openstack test project") |
| |
| # Create an user |
| user = keystone_conn.users.create( |
| acct.openstack.key, |
| password=acct.openstack.secret, |
| default_project=project) |
| |
| # Make the newly created user as ADMIN |
| admin_role = keystone_conn.roles.list(name="admin")[0] |
| keystone_conn.roles.grant( |
| admin_role.id, |
| user=user.id, |
| project=project.id) |
| |
| # nova API needs to be restarted, otherwise the new service doesn't play |
| # well |
| self.controller.ssh.exec_command("source keystonerc_admin && " |
| "service openstack-nova-api restart") |
| time.sleep(10) |
| |
| return acct |
| |
| def clear(self): |
| """Close out all SFTP connections. |
| """ |
| nodes = [self.controller] |
| nodes.extend(self.compute_nodes) |
| for node in nodes: |
| node.clear() |
| |
| |
| ############################################################################### |
| ## Begin pytests |
| ############################################################################### |
| |
| |
| @pytest.fixture(scope="session") |
| def cal(request): |
| """ |
| Loads rw.cal plugin via libpeas |
| """ |
| plugin = rw_peas.PeasPlugin('rwcal_openstack', 'RwCal-1.0') |
| engine, info, extension = plugin() |
| |
| # Get the RwLogger context |
| rwloggerctx = rwlogger.RwLog.Ctx.new("Cal-Log") |
| |
| cal = plugin.get_interface("Cloud") |
| try: |
| rc = cal.init(rwloggerctx) |
| assert rc == RwStatus.SUCCESS |
| except: |
| logger.error("ERROR:Cal plugin instantiation failed. Aborting tests") |
| else: |
| logger.info("Openstack Cal plugin successfully instantiated") |
| |
| return cal |
| |
| |
| @pytest.fixture(scope="session") |
| def account(request): |
| """Creates an openstack instance with 1 compute node and returns the newly |
| created account. |
| """ |
| cobbler = Cobbler("qacobbler.eng.riftio.com") |
| controller = Grunt("grunt3.qanet.riftio.com") |
| compute_nodes = [Grunt("grunt5.qanet.riftio.com")] |
| |
| test = OpenstackTest(cobbler, controller, compute_nodes) |
| account = test.start() |
| |
| request.addfinalizer(test.clear) |
| return account |
| |
| |
| def test_list_images(cal, account): |
| """Verify if 2 images are present |
| """ |
| status, resources = cal.get_image_list(account) |
| assert len(resources.imageinfo_list) == 2 |
| |
| def test_list_flavors(cal, account): |
| """Basic flavor checks |
| """ |
| status, resources = cal.get_flavor_list(account) |
| assert len(resources.flavorinfo_list) == 5 |
| |
| |
| class Template(object): |
| """A container to hold all cobbler related templates. |
| """ |
| GRUNT_CONFIG = """ |
| {host_name}) |
| CONTROLLER={ip} |
| BRGIF=1 |
| OVSDPDK=N |
| TRUSTED=N |
| QAT=N |
| HUGEPAGE=0 |
| VLAN=10:14 |
| PRIVATE_IP={private_ip} |
| FLOATING_IP={floating_ip} |
| DNS_SERVER={dns_server} |
| ;; |
| |
| """ |
| |
| SNIPPET_TEMPLATE = """ |
| # =====================Begining of snippet================= |
| # snippet openstack_test.cfg |
| case $name in |
| |
| {config} |
| |
| *) |
| ;; |
| esac |
| |
| # =====================End of snippet================= |
| |
| """ |
| |
| KS_TEMPATE = """ |
| $SNIPPET('rift-repos') |
| $SNIPPET('rift-base') |
| %packages |
| @core |
| wget |
| $SNIPPET('rift-grunt-fc21-packages') |
| ganglia-gmetad |
| ganglia-gmond |
| %end |
| |
| %pre |
| $SNIPPET('log_ks_pre') |
| $SNIPPET('kickstart_start') |
| # Enable installation monitoring |
| $SNIPPET('pre_anamon') |
| %end |
| |
| %post --log=/root/ks_post.log |
| $SNIPPET('openstack_test.cfg') |
| $SNIPPET('ganglia') |
| $SNIPPET('rift-post-yum') |
| $SNIPPET('rift-post') |
| $SNIPPET('rift_fix_grub') |
| |
| $SNIPPET('rdo-post') |
| echo "banner RDO test" >> /etc/profile |
| |
| $SNIPPET('kickstart_done') |
| %end |
| """ |