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