diff options
Diffstat (limited to 'tests/cloud_tests/instances')
-rw-r--r-- | tests/cloud_tests/instances/__init__.py | 10 | ||||
-rw-r--r-- | tests/cloud_tests/instances/base.py | 120 | ||||
-rw-r--r-- | tests/cloud_tests/instances/lxd.py | 121 |
3 files changed, 251 insertions, 0 deletions
diff --git a/tests/cloud_tests/instances/__init__.py b/tests/cloud_tests/instances/__init__.py new file mode 100644 index 00000000..85bea99f --- /dev/null +++ b/tests/cloud_tests/instances/__init__.py @@ -0,0 +1,10 @@ +# This file is part of cloud-init. See LICENSE file for license information. + + +def get_instance(snapshot, *args, **kwargs): + """ + get instance from snapshot + """ + return snapshot.launch(*args, **kwargs) + +# vi: ts=4 expandtab diff --git a/tests/cloud_tests/instances/base.py b/tests/cloud_tests/instances/base.py new file mode 100644 index 00000000..9559d286 --- /dev/null +++ b/tests/cloud_tests/instances/base.py @@ -0,0 +1,120 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +import os +import uuid + + +class Instance(object): + """ + Base instance object + """ + platform_name = None + + def __init__(self, name): + """ + setup + """ + self.name = name + + def execute(self, command, stdin=None, stdout=None, stderr=None, env={}): + """ + command: the command to execute as root inside the image + stdin, stderr, stdout: file handles + env: environment variables + + Execute assumes functional networking and execution as root with the + target filesystem being available at /. + + return_value: tuple containing stdout data, stderr data, exit code + """ + raise NotImplementedError + + def read_data(self, remote_path, encode=False): + """ + read_data from instance filesystem + remote_path: path in instance + decode: return as string + return_value: data as str or bytes + """ + raise NotImplementedError + + def write_data(self, remote_path, data): + """ + write data to instance filesystem + remote_path: path in instance + data: data to write, either str or bytes + """ + raise NotImplementedError + + def pull_file(self, remote_path, local_path): + """ + copy file at 'remote_path', from instance to 'local_path' + """ + with open(local_path, 'wb') as fp: + fp.write(self.read_data(remote_path), encode=True) + + def push_file(self, local_path, remote_path): + """ + copy file at 'local_path' to instance at 'remote_path' + """ + with open(local_path, 'rb') as fp: + self.write_data(remote_path, fp.read()) + + def run_script(self, script): + """ + run script in target and return stdout + """ + script_path = os.path.join('/tmp', str(uuid.uuid1())) + self.write_data(script_path, script) + (out, err, exit_code) = self.execute(['/bin/bash', script_path]) + return out + + def console_log(self): + """ + return_value: bytes of this instance’s console + """ + raise NotImplementedError + + def reboot(self, wait=True): + """ + reboot instance + """ + raise NotImplementedError + + def shutdown(self, wait=True): + """ + shutdown instance + """ + raise NotImplementedError + + def start(self, wait=True): + """ + start instance + """ + raise NotImplementedError + + def destroy(self): + """ + clean up instance + """ + pass + + def _wait_for_cloud_init(self, wait_time): + """ + wait until system has fully booted and cloud-init has finished + """ + if not wait_time: + return + + found_msg = 'found' + cmd = ('for ((i=0;i<{wait};i++)); do [ -f "{file}" ] && ' + '{{ echo "{msg}";break; }} || sleep 1; done').format( + file='/run/cloud-init/result.json', + wait=wait_time, msg=found_msg) + + (out, err, exit) = self.execute(['/bin/bash', '-c', cmd]) + if out.strip() != found_msg: + raise OSError('timeout: after {}s, cloud-init has not started' + .format(wait_time)) + +# vi: ts=4 expandtab diff --git a/tests/cloud_tests/instances/lxd.py b/tests/cloud_tests/instances/lxd.py new file mode 100644 index 00000000..f0aa1214 --- /dev/null +++ b/tests/cloud_tests/instances/lxd.py @@ -0,0 +1,121 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from tests.cloud_tests.instances import base + + +class LXDInstance(base.Instance): + """ + LXD container backed instance + """ + platform_name = "lxd" + + def __init__(self, name, platform, pylxd_container): + """ + setup + """ + self.platform = platform + self._pylxd_container = pylxd_container + super(LXDInstance, self).__init__(name) + + @property + def pylxd_container(self): + self._pylxd_container.sync() + return self._pylxd_container + + def execute(self, command, stdin=None, stdout=None, stderr=None, env={}): + """ + command: the command to execute as root inside the image + stdin, stderr, stdout: file handles + env: environment variables + + Execute assumes functional networking and execution as root with the + target filesystem being available at /. + + return_value: tuple containing stdout data, stderr data, exit code + """ + # TODO: the pylxd api handler for container.execute needs to be + # extended to properly pass in stdin + # TODO: the pylxd api handler for container.execute needs to be + # extended to get the return code, for now just use 0 + self.start() + if stdin: + raise NotImplementedError + res = self.pylxd_container.execute(command, environment=env) + for (f, data) in (i for i in zip((stdout, stderr), res) if i[0]): + f.write(data) + return res + (0,) + + def read_data(self, remote_path, decode=False): + """ + read data from instance filesystem + remote_path: path in instance + decode: return as string + return_value: data as str or bytes + """ + data = self.pylxd_container.files.get(remote_path) + return data.decode() if decode and isinstance(data, bytes) else data + + def write_data(self, remote_path, data): + """ + write data to instance filesystem + remote_path: path in instance + data: data to write, either str or bytes + """ + self.pylxd_container.files.put(remote_path, data) + + def console_log(self): + """ + return_value: bytes of this instance’s console + """ + raise NotImplementedError + + def reboot(self, wait=True): + """ + reboot instance + """ + self.shutdown(wait=wait) + self.start(wait=wait) + + def shutdown(self, wait=True): + """ + shutdown instance + """ + if self.pylxd_container.status != 'Stopped': + self.pylxd_container.stop(wait=wait) + + def start(self, wait=True, wait_time=None): + """ + start instance + """ + if self.pylxd_container.status != 'Running': + self.pylxd_container.start(wait=wait) + if wait and isinstance(wait_time, int): + self._wait_for_cloud_init(wait_time) + + def freeze(self): + """ + freeze instance + """ + if self.pylxd_container.status != 'Frozen': + self.pylxd_container.freeze(wait=True) + + def unfreeze(self): + """ + unfreeze instance + """ + if self.pylxd_container.status == 'Frozen': + self.pylxd_container.unfreeze(wait=True) + + def destroy(self): + """ + clean up instance + """ + self.unfreeze() + self.shutdown() + self.pylxd_container.delete(wait=True) + if self.platform.container_exists(self.name): + raise OSError('container {} was not properly removed' + .format(self.name)) + super(LXDInstance, self).destroy() + +# vi: ts=4 expandtab |