From 2188d8d633813d670a39b0914aec6f1e44285b81 Mon Sep 17 00:00:00 2001 From: Leo Simons Date: Wed, 6 Aug 2014 13:18:50 +0200 Subject: [PATCH] Pure python tests for systemvm This approach is instead of serverspec, but filling the same purpose. It's main advantage is that it uses nose and python, just like the existing marvin-based integration test suite. --- test/systemvm/README.md | 56 ++++++++++ test/systemvm/__init__.py | 149 +++++++++++++++++++++++++++ test/systemvm/test_hello_systemvm.py | 51 +++++++++ tools/vagrant/systemvm/Vagrantfile | 17 ++- 4 files changed, 264 insertions(+), 9 deletions(-) create mode 100644 test/systemvm/README.md create mode 100644 test/systemvm/__init__.py create mode 100644 test/systemvm/test_hello_systemvm.py diff --git a/test/systemvm/README.md b/test/systemvm/README.md new file mode 100644 index 00000000000..7473fb8863f --- /dev/null +++ b/test/systemvm/README.md @@ -0,0 +1,56 @@ +Requirements +============ +To run these tests, first get the vagrant setup for the systemvm working, +see ../../tools/vagrant/systemvm. + +Then, install dependencies + + pip install nose paramiko python-vagrant envassert cuisine + +Running tests +============= +Then run the tests using your favorite python unittest runner + + nosetests-2.7 + +If you have already started the systemvm with 'vagrant up', that VM will get +used for all the tests. + +If you have not started the systemvm yet, it will be started and stopped for +every test case. That's nice for test isolation, but it's very slow, so it is +not recommended. + +You can also run these tests out of the box with PyDev or PyCharm or whatever. + +Adding tests +============ +Simply create new test_xxx.py files with test cases that extend from +SystemVMTestCase. + +Use [envassert](https://pypi.python.org/pypi/envassert) checks to define +your test assertions. + +Use [cuisine](https://pypi.python.org/pypi/cuisine), +[fab](https://pypi.python.org/pypi/Fabric), or +[paramiko](https://pypi.python.org/pypi/paramiko) to otherwise interact with +the systemvm. When you do, please consider creating your own little wrappers +around fab run. I.e. the pattern is + +``` +from __future__ import with_statement +from fabric.api import run, hide + +def something_to_do(argument): + with hide("everything"): + result = run("do something %s" % argument).wrangle() + return "expected" in result +``` + +for a new kind of check and then in your test + +``` +class HelloSystemVMTestCase(SystemVMTestCase): + @attr(tags=["systemvm"], required_hardware="true") + def test_something(self): + assert something_to_do('foo') +``` diff --git a/test/systemvm/__init__.py b/test/systemvm/__init__.py new file mode 100644 index 00000000000..3c4dd4b956d --- /dev/null +++ b/test/systemvm/__init__.py @@ -0,0 +1,149 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +from vagrant import Vagrant +from unittest import TestCase +from paramiko.config import SSHConfig +from paramiko.client import SSHClient, AutoAddPolicy +from fabric.api import env +from envassert import file, detect + +from StringIO import StringIO + +from nose.plugins.attrib import attr + +import os.path + + +_defaultVagrantDir = os.path.abspath(os.path.join( + os.path.basename(__file__), '..', '..', '..', 'tools', 'vagrant', 'systemvm')) + + +class SystemVM(object): + def __init__(self, + host='default', + vagrantDir=None, + controlVagrant=True): + global _defaultVagrantDir + self.host = host + self._controlVagrant = controlVagrant + if vagrantDir is None: + vagrantDir = _defaultVagrantDir + self._vagrant = Vagrant(root=vagrantDir) + self._startedVagrant = False + self._sshClient = None + self._sshConfigStr = None + self._sshConfig = None + self._sshHostConfig = None + + def maybeUp(self): + if not self._controlVagrant: + return + state = self._vagrant.status(vm_name=self.host)[0].state + if state == Vagrant.NOT_CREATED: + self._vagrant.up(vm_name=self.host) + self._startedVagrant = True + elif state in [Vagrant.POWEROFF, Vagrant.SAVED, Vagrant.ABORTED]: + raise Exception( + "SystemVM testing does not support resume(), do not use vagrant suspend/halt") + elif state == Vagrant.RUNNING: + self._startedVagrant = False + else: + raise Exception("Unrecognized vagrant state %s" % state) + + def maybeDestroy(self): + if not self._controlVagrant or not self._startedVagrant: + return + self._vagrant.destroy(vm_name=self.host) + if self._sshClient is not None: + self._sshClient.close() + + def loadSshConfig(self): + if self._sshConfig is None: + self._sshConfigStr = self._vagrant.ssh_config(vm_name=self.host) + configObj = StringIO(self._sshConfigStr) + self._sshConfig = SSHConfig() + # noinspection PyTypeChecker + self._sshConfig.parse(configObj) + self._sshHostConfig = self._sshConfig.lookup(self.host) + + @property + def sshConfig(self): + if self._sshConfig is None: + self.loadSshConfig() + return self._sshConfig + + @property + def sshConfigStr(self): + if self._sshConfigStr is None: + self.loadSshConfig() + return self._sshConfigStr + + @property + def sshClient(self): + if self._sshClient is None: + self.loadSshConfig() + self._sshClient = SSHClient() + self._sshClient.set_missing_host_key_policy(AutoAddPolicy()) + self._sshClient.connect(self.hostname, self.sshPort, self.sshUser, + key_filename=self.sshKey, timeout=10) + return self._sshClient + + @property + def hostname(self): + return self._sshHostConfig.get('hostname', self.host) + + @property + def sshPort(self): + return int(self._sshHostConfig.get('port', 22)) + + @property + def sshUser(self): + return self._sshHostConfig.get('user', 'root') + + @property + def sshKey(self): + return self._sshHostConfig.get('identityfile', '~/.ssh/id_rsa') + + +class SystemVMTestCase(TestCase): + @classmethod + def setUpClass(cls): + cls.systemvm = SystemVM() + cls.systemvm.maybeUp() + + @classmethod + def tearDownClass(cls): + # noinspection PyUnresolvedReferences + cls.systemvm.maybeDestroy() + + def setUp(self): + self.sshClient = self.systemvm.sshClient + # self._env_host_string_orig = env.host_string + env.host_string = "%s:%s" % (self.systemvm.hostname, self.systemvm.sshPort) + env.user = self.systemvm.sshUser + env.port = self.systemvm.sshPort + env.key_filename = self.systemvm.sshKey + env.use_ssh_config = True + env.abort_on_prompts = True + env.command_timeout = 10 + env.timeout = 5 + env.platform_family = detect.detect() + + # this could break down when executing multiple test cases in parallel in the same python process + # def tearDown(self): + # env.host_string = self._env_host_string_orig diff --git a/test/systemvm/test_hello_systemvm.py b/test/systemvm/test_hello_systemvm.py new file mode 100644 index 00000000000..ce33c3be960 --- /dev/null +++ b/test/systemvm/test_hello_systemvm.py @@ -0,0 +1,51 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +"""Example of using paramiko and envassert for systemvm tests.""" + +from nose.plugins.attrib import attr +from envassert import file, package, user +from cuisine import file_write +try: + from . import SystemVMTestCase +except (ImportError, ValueError): + from systemvm import SystemVMTestCase + + +class HelloSystemVMTestCase(SystemVMTestCase): + @attr(tags=["systemvm"], required_hardware="true") + def test_hello_systemvm_paramiko(self): + """Test we can connect to the systemvm over ssh, low-level with paramiko""" + stdin, stdout, stderr = self.sshClient.exec_command('echo hello') + result = stdout.read().strip() + self.assertEqual('hello', result) + + @attr(tags=["systemvm"], required_hardware="true") + def test_hello_systemvm_envassert(self): + """Test we can run envassert assertions on the systemvm""" + assert file.exists('/etc/hosts') + + for packageName in ['dnsmasq', 'haproxy', 'keepalived', 'curl']: + assert package.installed(packageName) + + assert user.exists('cloud') + + @attr(tags=["systemvm"], required_hardware="true") + def test_hello_systemvm_cuisine(self): + """Test we can run cuisine on the systemvm""" + file_write('/tmp/run_cuisine', 'success!\n') + assert file.has_line('/tmp/run_cuisine', 'success!') diff --git a/tools/vagrant/systemvm/Vagrantfile b/tools/vagrant/systemvm/Vagrantfile index 78c9bb47490..56f3e0b3aa5 100644 --- a/tools/vagrant/systemvm/Vagrantfile +++ b/tools/vagrant/systemvm/Vagrantfile @@ -22,13 +22,12 @@ include RbConfig VAGRANTFILE_API_VERSION = '2' -unless ENV['VPC_IP'] - puts 'Please specify the VPC IP by settings the VPC_IP environment variable' - puts 'Example: export VPC_IP=192.168.56.30' - puts '' - exit 1 +if ENV['VPC_IP'] + puts 'You did not specify the VPC IP by settings the VPC_IP environment variable' + puts 'Using the default VPC_IP=192.168.56.30' end -VPC_NAME='r-' + ENV['VPC_IP'].split('.').last + '-VM' +VPC_IP = ENV['VPC_IP'] || '192.168.56.30' +VPC_NAME='r-' + VPC_IP.split('.').last + '-VM' if ARGV[0] == 'up' iso_util='' @@ -69,12 +68,12 @@ end Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.box = 'cloudstack/systemvm' - config.vm.network 'private_network', ip: ENV['VPC_IP'], auto_config: false + config.vm.network 'private_network', ip: VPC_IP, auto_config: false config.vm.synced_folder 'vagrant', '/vagrant', disabled: true config.ssh.forward_agent = true config.ssh.username = 'root' - config.ssh.host = ENV['VPC_IP'] + config.ssh.host = VPC_IP config.ssh.port = 3922 config.ssh.guest_port = 3922 @@ -87,7 +86,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| '--medium', './systemvm.iso'] vb.customize('pre-boot', ['modifyvm', :id, '--nic1', 'none']) extra_data='cmdline:console=hvc0 vpccidr=172.16.0.0/16 domain=devcloud.local dns1=8.8.8.8 dns2=8.8.8.4' + - " template=domP name=#{VPC_NAME} eth0ip=#{ENV['VPC_IP']}" + + " template=domP name=#{VPC_NAME} eth0ip=#{VPC_IP}" + ' eth0mask=255.255.255.0 type=vpcrouter disable_rp_filter=true' vb.customize('pre-boot', ['setextradata', :id, 'VBoxInternal/Devices/pcbios/0/Config/DmiOEMVBoxRev', extra_data]) end