Skip to content

Commit

Permalink
tests: basic unit testing on incus_instance (#17)
Browse files Browse the repository at this point in the history
- includes crude mocking of incuscli
  • Loading branch information
kmpm authored Nov 29, 2024
1 parent 8ea568d commit 23a38e1
Show file tree
Hide file tree
Showing 12 changed files with 716 additions and 1 deletion.
6 changes: 6 additions & 0 deletions DEVELOP.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,13 @@ ansible-test sanity --python 3.11 -v
# test with local venv
ansible-test units --venv --python 3.11

# with docker
ansible-test units --docker -v

# run integrations tests on your local incus installation
ansible-test integration --venv --python 3.11 unsupported/incus_instance


```


Expand Down
24 changes: 24 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

.PHONY: clean
clean:
@echo "Cleaning up..."
@echo "Remove tests/output directories"
rm -rf tests/output

@echo "Remove all python related directories"
find . -type d -name __pycache__ -exec rm -rf {} \;
rm -rf .mypy_cache


.PHONY: test
test: test/sanity test/units


.PHONY: test/sanity
test/sanity:
ansible-test sanity --python 3.11 -v


.PHONY: test/units
test/units:
ansible-test units --venv --python 3.11
22 changes: 21 additions & 1 deletion tests/integration/targets/incus_instance/tasks/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
source:
type: image
alias: debian/12/cloud
server: "https://images.incus.org"
server: "https://images.linuxcontainers.org"
protocol: "simplestreams"
mode: "pull"
allow_inconsistent: false
Expand All @@ -17,6 +17,19 @@
state: absent
register: destroy_result

# - name: create vm
# kmpm.incus.incus_instance:
# name: myvm
# type: virtual-machine
# source:
# type: image
# alias: debian/12/cloud
# server: "https://images.linuxcontainers.org"
# protocol: "simplestreams"
# mode: "pull"
# allow_inconsistent: false
# register: create_vm_result

- name: check create_result
assert:
that:
Expand All @@ -30,3 +43,10 @@
- destroy_result.changed == True
- destroy_result.actions == ['stop', 'delete']
- not destroy_result.failed

# - name: check create_vm_result
# assert:
# that:
# - create_vm_result.changed == True
# - create_vm_result.actions == ['create', 'start']
# - not create_vm_result.failed
172 changes: 172 additions & 0 deletions tests/unit/plugins/modules/SAMPLE_OUTPUT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Sample responses

This document lists sample responses using `incus query` for different tasks and
is intended to help out during development.

## POST instance response

Creating a container instance called `testinstance`.

```json
{
"type": "sync",
"status": "Success",
"status_code": 200,
"operation": "",
"error_code": 0,
"error": "",
"metadata": {
"id": "a252b7bd-7178-42ca-8d94-1680254eec7b",
"class": "task",
"description": "Creating instance",
"created_at": "2024-11-28T23:11:49.303545747+01:00",
"updated_at": "2024-11-28T23:11:50.724932671+01:00",
"status": "Success",
"status_code": 200,
"resources": {
"instances": [
"/1.0/instances/testinstance"
]
},
"metadata": {
"create_instance_from_image_unpack_progress": "Unpacking image: 100% (3.97GB/s)",
"progress": {
"percent": "100",
"speed": "3972972972",
"stage": "create_instance_from_image_unpack"
}
},
"may_cancel": false,
"err": "",
"location": "none"
}
}
```

## Get absent instance

```json
{
"type": "error",
"status":"",
"status_code": 0,
"operation":"",
"error_code":404,
"error":"Not Found",
"metadata":null
}
```

## GET state

Using the path `/1.0/instances/testinstance/state?project=default`

```json
{
"type": "sync",
"status": "Success",
"status_code": 200,
"operation": "",
"error_code": 0,
"error": "",
"metadata": {
"status": "Stopped",
"status_code": 102,
"disk": {},
"memory": {
"usage": 0,
"usage_peak": 0,
"total": 0,
"swap_usage": 0,
"swap_usage_peak": 0
},
"network": null,
"pid": 0,
"processes": 0,
"cpu": {
"usage": 0
},
"started_at": "0001-01-01T00:00:00Z",
"os_info": null
}
}
```

## Get existing container

```json
{
"type": "sync",
"status": "Success",
"status_code": 200,
"operation": "",
"error_code": 0,
"error": "",
"metadata": {
"architecture": "aarch64",
"config": {
"image.architecture": "arm64",
"image.description": "Debian bookworm arm64 (20241128_05:24)",
"image.os": "Debian",
"image.release": "bookworm",
"image.serial": "20241128_05:24",
"image.type": "squashfs",
"image.variant": "cloud",
"volatile.apply_template": "create",
"volatile.base_image": "5bfec5b4d6362bbf8755f637119ac3de7c15ca267573e47c9873388a5655e196",
"volatile.cloud-init.instance-id": "4f144556-ea4c-4154-8a57-91ff4346e5e1",
"volatile.eth0.hwaddr": "00:16:3e:9a:00:37",
"volatile.idmap.base": "0",
"volatile.idmap.next": "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000}]",
"volatile.last_state.idmap": "[]",
"volatile.uuid": "321b7285-2c66-4b62-924b-0c47d61ea7a0",
"volatile.uuid.generation": "321b7285-2c66-4b62-924b-0c47d61ea7a0"
},
"devices": {},
"ephemeral": false,
"profiles": [
"default"
],
"stateful": false,
"description": "",
"created_at": "2024-11-28T22:11:50.701255415Z",
"expanded_config": {
"image.architecture": "arm64",
"image.description": "Debian bookworm arm64 (20241128_05:24)",
"image.os": "Debian",
"image.release": "bookworm",
"image.serial": "20241128_05:24",
"image.type": "squashfs",
"image.variant": "cloud",
"volatile.apply_template": "create",
"volatile.base_image": "5bfec5b4d6362bbf8755f637119ac3de7c15ca267573e47c9873388a5655e196",
"volatile.cloud-init.instance-id": "4f144556-ea4c-4154-8a57-91ff4346e5e1",
"volatile.eth0.hwaddr": "00:16:3e:9a:00:37",
"volatile.idmap.base": "0",
"volatile.idmap.next": "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":1000000,\"Nsid\":0,\"Maprange\":1000000000}]",
"volatile.last_state.idmap": "[]",
"volatile.uuid": "321b7285-2c66-4b62-924b-0c47d61ea7a0",
"volatile.uuid.generation": "321b7285-2c66-4b62-924b-0c47d61ea7a0"
},
"expanded_devices": {
"eth0": {
"name": "eth0",
"network": "incusbr0",
"type": "nic"
},
"root": {
"path": "/",
"pool": "default",
"type": "disk"
}
},
"name": "testinstance",
"status": "Stopped",
"status_code": 102,
"last_used_at": "1970-01-01T00:00:00Z",
"location": "none",
"type": "container",
"project": "default"
}
}
```
145 changes: 145 additions & 0 deletions tests/unit/plugins/modules/clients.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import json
import os
from ansible.module_utils.six.moves.urllib.parse import urlparse, parse_qs


fixturedir = os.path.join(os.path.dirname(__file__), 'fixtures')
status_messages = {
0: '',
200: 'Success',
}

error_messages = {
0: '',
404: 'Not Found',
500: 'Internal Server Error',
501: 'Not Implemented',
}


def load_fixture(name):
with open(os.path.join(fixturedir, name)) as f:
return json.load(f)


def generate_response(**kwargs):
resp = {
'type': 'sync',
'status': '',
'status_code': 0,
'operation': '',
'error_code': 0,
'error': '',
'metadata': None
}
for k, v in kwargs.items():
if k in resp:
resp[k] = v

resp['status'] = status_messages.get(resp['status_code'], '')
if resp['error_code']:
resp['error'] = error_messages.get(resp['error_code'], '')
resp['type'] = 'error'

return resp


class MockClient:
instances = {}
counter = 0
request = {}

def _response(self, value):
stringval = json.dumps(value)
print("response", dict(requst=self.request, response=stringval))
return stringval

def _instances_api(self, method, segments, queryargs, data):
name = segments[0] if len(segments) > 0 else None
if not name:
if method == 'GET':
return json.dumps([])
elif method == 'POST':
# create instance
name = data['name']
metadata = load_fixture('get_instance.json')['metadata']
metadata['name'] = name
metadata['project'] = queryargs.get('project', ['default'])[0]
self.instances[name] = metadata
resp = load_fixture('post_instance.json')
self.counter += 1
resp['metadata']['id'] = '1337-42-%d' % self.counter
resp['metadata']['name'] = metadata['name']
resp['metadata']['project'] = metadata['project']
# print("creates instance", name, resp['metadata'])
return self._response(resp)

segments = segments[1:]
if segments and segments[0] == 'state':
segments = segments[1:]
if method == 'GET':
# return named instance state if any
if name in self.instances:
resp = load_fixture('get_instance_state.json')
metadata = self.instances[name]
resp['metadata'] = self.instances[name]['state']
return self._response(resp)
else:
return json.dumps(generate_response(error_code=404))
elif method == 'PUT':
# update state
if name in self.instances:
resp = load_fixture('put_instance_state.json')
self.instances[name]['status'] = "Running"
self.instances[name]['status_code'] = "103"

return self._response(resp)
else:
return self._response(generate_response(error_code=404))
else:
if method == 'GET':
# return named instance if any
if name in self.instances:
resp = generate_response(status_code=200)
resp['metadata'] = self.instances[name]
return self._response(resp)
else:
return self._response(generate_response(error_code=404))

raise Exception('instance action: %s %r' % (method, segments,))

def execute(self, client, *args, **kwargs):
# if client.debug:
client.debug = True
client.logs.append(args)
method = args[2]
querypath = args[3]
# extract queryargs from querypath
p = urlparse(querypath)
queryargs = parse_qs(p.query)
data = None
for i, arg in enumerate(args):
if arg == '--data':
data = json.loads(args[i + 1])

self.request = dict(path=querypath, data=data)
# extract segments from querypath
segments = p.path.split('/')
# remove empty first segment
segments = segments[1:]
# is the first segment '1.0'?
if segments[0] != '1.0':
raise Exception('Unknown version: %s' % segments)

# remove version segment
segments = segments[1:]

if segments[0] == 'instances':
return self._instances_api(method, segments[1:], queryargs, data)

else:
print("log", client.logs)
raise Exception('Unknown method or querypath: %s %s' % (method, querypath,))
Loading

0 comments on commit 23a38e1

Please sign in to comment.