--- /dev/null
+"""
+#
+#
+# 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
+"""