Skip to content

Commit 2188d8d

Browse files
lsimonswilderrodrigues
authored andcommitted
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.
1 parent 666dc16 commit 2188d8d

4 files changed

Lines changed: 264 additions & 9 deletions

File tree

test/systemvm/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
Requirements
2+
============
3+
To run these tests, first get the vagrant setup for the systemvm working,
4+
see ../../tools/vagrant/systemvm.
5+
6+
Then, install dependencies
7+
8+
pip install nose paramiko python-vagrant envassert cuisine
9+
10+
Running tests
11+
=============
12+
Then run the tests using your favorite python unittest runner
13+
14+
nosetests-2.7
15+
16+
If you have already started the systemvm with 'vagrant up', that VM will get
17+
used for all the tests.
18+
19+
If you have not started the systemvm yet, it will be started and stopped for
20+
every test case. That's nice for test isolation, but it's very slow, so it is
21+
not recommended.
22+
23+
You can also run these tests out of the box with PyDev or PyCharm or whatever.
24+
25+
Adding tests
26+
============
27+
Simply create new test_xxx.py files with test cases that extend from
28+
SystemVMTestCase.
29+
30+
Use [envassert](https://pypi.python.org/pypi/envassert) checks to define
31+
your test assertions.
32+
33+
Use [cuisine](https://pypi.python.org/pypi/cuisine),
34+
[fab](https://pypi.python.org/pypi/Fabric), or
35+
[paramiko](https://pypi.python.org/pypi/paramiko) to otherwise interact with
36+
the systemvm. When you do, please consider creating your own little wrappers
37+
around fab run. I.e. the pattern is
38+
39+
```
40+
from __future__ import with_statement
41+
from fabric.api import run, hide
42+
43+
def something_to_do(argument):
44+
with hide("everything"):
45+
result = run("do something %s" % argument).wrangle()
46+
return "expected" in result
47+
```
48+
49+
for a new kind of check and then in your test
50+
51+
```
52+
class HelloSystemVMTestCase(SystemVMTestCase):
53+
@attr(tags=["systemvm"], required_hardware="true")
54+
def test_something(self):
55+
assert something_to_do('foo')
56+
```

test/systemvm/__init__.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
from vagrant import Vagrant
19+
from unittest import TestCase
20+
from paramiko.config import SSHConfig
21+
from paramiko.client import SSHClient, AutoAddPolicy
22+
from fabric.api import env
23+
from envassert import file, detect
24+
25+
from StringIO import StringIO
26+
27+
from nose.plugins.attrib import attr
28+
29+
import os.path
30+
31+
32+
_defaultVagrantDir = os.path.abspath(os.path.join(
33+
os.path.basename(__file__), '..', '..', '..', 'tools', 'vagrant', 'systemvm'))
34+
35+
36+
class SystemVM(object):
37+
def __init__(self,
38+
host='default',
39+
vagrantDir=None,
40+
controlVagrant=True):
41+
global _defaultVagrantDir
42+
self.host = host
43+
self._controlVagrant = controlVagrant
44+
if vagrantDir is None:
45+
vagrantDir = _defaultVagrantDir
46+
self._vagrant = Vagrant(root=vagrantDir)
47+
self._startedVagrant = False
48+
self._sshClient = None
49+
self._sshConfigStr = None
50+
self._sshConfig = None
51+
self._sshHostConfig = None
52+
53+
def maybeUp(self):
54+
if not self._controlVagrant:
55+
return
56+
state = self._vagrant.status(vm_name=self.host)[0].state
57+
if state == Vagrant.NOT_CREATED:
58+
self._vagrant.up(vm_name=self.host)
59+
self._startedVagrant = True
60+
elif state in [Vagrant.POWEROFF, Vagrant.SAVED, Vagrant.ABORTED]:
61+
raise Exception(
62+
"SystemVM testing does not support resume(), do not use vagrant suspend/halt")
63+
elif state == Vagrant.RUNNING:
64+
self._startedVagrant = False
65+
else:
66+
raise Exception("Unrecognized vagrant state %s" % state)
67+
68+
def maybeDestroy(self):
69+
if not self._controlVagrant or not self._startedVagrant:
70+
return
71+
self._vagrant.destroy(vm_name=self.host)
72+
if self._sshClient is not None:
73+
self._sshClient.close()
74+
75+
def loadSshConfig(self):
76+
if self._sshConfig is None:
77+
self._sshConfigStr = self._vagrant.ssh_config(vm_name=self.host)
78+
configObj = StringIO(self._sshConfigStr)
79+
self._sshConfig = SSHConfig()
80+
# noinspection PyTypeChecker
81+
self._sshConfig.parse(configObj)
82+
self._sshHostConfig = self._sshConfig.lookup(self.host)
83+
84+
@property
85+
def sshConfig(self):
86+
if self._sshConfig is None:
87+
self.loadSshConfig()
88+
return self._sshConfig
89+
90+
@property
91+
def sshConfigStr(self):
92+
if self._sshConfigStr is None:
93+
self.loadSshConfig()
94+
return self._sshConfigStr
95+
96+
@property
97+
def sshClient(self):
98+
if self._sshClient is None:
99+
self.loadSshConfig()
100+
self._sshClient = SSHClient()
101+
self._sshClient.set_missing_host_key_policy(AutoAddPolicy())
102+
self._sshClient.connect(self.hostname, self.sshPort, self.sshUser,
103+
key_filename=self.sshKey, timeout=10)
104+
return self._sshClient
105+
106+
@property
107+
def hostname(self):
108+
return self._sshHostConfig.get('hostname', self.host)
109+
110+
@property
111+
def sshPort(self):
112+
return int(self._sshHostConfig.get('port', 22))
113+
114+
@property
115+
def sshUser(self):
116+
return self._sshHostConfig.get('user', 'root')
117+
118+
@property
119+
def sshKey(self):
120+
return self._sshHostConfig.get('identityfile', '~/.ssh/id_rsa')
121+
122+
123+
class SystemVMTestCase(TestCase):
124+
@classmethod
125+
def setUpClass(cls):
126+
cls.systemvm = SystemVM()
127+
cls.systemvm.maybeUp()
128+
129+
@classmethod
130+
def tearDownClass(cls):
131+
# noinspection PyUnresolvedReferences
132+
cls.systemvm.maybeDestroy()
133+
134+
def setUp(self):
135+
self.sshClient = self.systemvm.sshClient
136+
# self._env_host_string_orig = env.host_string
137+
env.host_string = "%s:%s" % (self.systemvm.hostname, self.systemvm.sshPort)
138+
env.user = self.systemvm.sshUser
139+
env.port = self.systemvm.sshPort
140+
env.key_filename = self.systemvm.sshKey
141+
env.use_ssh_config = True
142+
env.abort_on_prompts = True
143+
env.command_timeout = 10
144+
env.timeout = 5
145+
env.platform_family = detect.detect()
146+
147+
# this could break down when executing multiple test cases in parallel in the same python process
148+
# def tearDown(self):
149+
# env.host_string = self._env_host_string_orig
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
"""Example of using paramiko and envassert for systemvm tests."""
19+
20+
from nose.plugins.attrib import attr
21+
from envassert import file, package, user
22+
from cuisine import file_write
23+
try:
24+
from . import SystemVMTestCase
25+
except (ImportError, ValueError):
26+
from systemvm import SystemVMTestCase
27+
28+
29+
class HelloSystemVMTestCase(SystemVMTestCase):
30+
@attr(tags=["systemvm"], required_hardware="true")
31+
def test_hello_systemvm_paramiko(self):
32+
"""Test we can connect to the systemvm over ssh, low-level with paramiko"""
33+
stdin, stdout, stderr = self.sshClient.exec_command('echo hello')
34+
result = stdout.read().strip()
35+
self.assertEqual('hello', result)
36+
37+
@attr(tags=["systemvm"], required_hardware="true")
38+
def test_hello_systemvm_envassert(self):
39+
"""Test we can run envassert assertions on the systemvm"""
40+
assert file.exists('/etc/hosts')
41+
42+
for packageName in ['dnsmasq', 'haproxy', 'keepalived', 'curl']:
43+
assert package.installed(packageName)
44+
45+
assert user.exists('cloud')
46+
47+
@attr(tags=["systemvm"], required_hardware="true")
48+
def test_hello_systemvm_cuisine(self):
49+
"""Test we can run cuisine on the systemvm"""
50+
file_write('/tmp/run_cuisine', 'success!\n')
51+
assert file.has_line('/tmp/run_cuisine', 'success!')

tools/vagrant/systemvm/Vagrantfile

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,12 @@ include RbConfig
2222

2323
VAGRANTFILE_API_VERSION = '2'
2424

25-
unless ENV['VPC_IP']
26-
puts 'Please specify the VPC IP by settings the VPC_IP environment variable'
27-
puts 'Example: export VPC_IP=192.168.56.30'
28-
puts ''
29-
exit 1
25+
if ENV['VPC_IP']
26+
puts 'You did not specify the VPC IP by settings the VPC_IP environment variable'
27+
puts 'Using the default VPC_IP=192.168.56.30'
3028
end
31-
VPC_NAME='r-' + ENV['VPC_IP'].split('.').last + '-VM'
29+
VPC_IP = ENV['VPC_IP'] || '192.168.56.30'
30+
VPC_NAME='r-' + VPC_IP.split('.').last + '-VM'
3231

3332
if ARGV[0] == 'up'
3433
iso_util=''
@@ -69,12 +68,12 @@ end
6968

7069
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
7170
config.vm.box = 'cloudstack/systemvm'
72-
config.vm.network 'private_network', ip: ENV['VPC_IP'], auto_config: false
71+
config.vm.network 'private_network', ip: VPC_IP, auto_config: false
7372
config.vm.synced_folder 'vagrant', '/vagrant', disabled: true
7473

7574
config.ssh.forward_agent = true
7675
config.ssh.username = 'root'
77-
config.ssh.host = ENV['VPC_IP']
76+
config.ssh.host = VPC_IP
7877
config.ssh.port = 3922
7978
config.ssh.guest_port = 3922
8079

@@ -87,7 +86,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
8786
'--medium', './systemvm.iso']
8887
vb.customize('pre-boot', ['modifyvm', :id, '--nic1', 'none'])
8988
extra_data='cmdline:console=hvc0 vpccidr=172.16.0.0/16 domain=devcloud.local dns1=8.8.8.8 dns2=8.8.8.4' +
90-
" template=domP name=#{VPC_NAME} eth0ip=#{ENV['VPC_IP']}" +
89+
" template=domP name=#{VPC_NAME} eth0ip=#{VPC_IP}" +
9190
' eth0mask=255.255.255.0 type=vpcrouter disable_rp_filter=true'
9291
vb.customize('pre-boot', ['setextradata', :id, 'VBoxInternal/Devices/pcbios/0/Config/DmiOEMVBoxRev', extra_data])
9392
end

0 commit comments

Comments
 (0)