Remove the local tempest plugin
The plugin has been split into its own repository [1] in accordance with Queens Goal "Split Tempest Plugins into Separate Repos/Projects" [2]. This patch removes the local copy as well as the setuptools entry point. [1] https://github.com/openstack/zun-tempest-plugin [2] https://governance.openstack.org/tc/goals/queens/ split-tempest-plugins.html Implements: blueprint zun-split-tempest-plugins Change-Id: Ibfeb38b52c05a9f886dc7524d892842971cbd264
This commit is contained in:
parent
4d91ab84bb
commit
83b7fb110e
@ -32,6 +32,9 @@ set +o xtrace
|
|||||||
ZUN_REPO=${ZUN_REPO:-${GIT_BASE}/openstack/zun.git}
|
ZUN_REPO=${ZUN_REPO:-${GIT_BASE}/openstack/zun.git}
|
||||||
ZUN_BRANCH=${ZUN_BRANCH:-master}
|
ZUN_BRANCH=${ZUN_BRANCH:-master}
|
||||||
ZUN_DIR=$DEST/zun
|
ZUN_DIR=$DEST/zun
|
||||||
|
ZUN_TEMPEST_PLUGIN_REPO=${ZUN_REPO:-${GIT_BASE}/openstack/zun-tempest-plugin.git}
|
||||||
|
ZUN_TEMPEST_PLUGIN_BRANCH=${ZUN_BRANCH:-master}
|
||||||
|
ZUN_TEMPEST_PLUGIN_DIR=$DEST/zun-tempest-plugin
|
||||||
|
|
||||||
GITREPO["python-zunclient"]=${ZUNCLIENT_REPO:-${GIT_BASE}/openstack/python-zunclient.git}
|
GITREPO["python-zunclient"]=${ZUNCLIENT_REPO:-${GIT_BASE}/openstack/python-zunclient.git}
|
||||||
GITBRANCH["python-zunclient"]=${ZUNCLIENT_BRANCH:-master}
|
GITBRANCH["python-zunclient"]=${ZUNCLIENT_BRANCH:-master}
|
||||||
@ -296,6 +299,9 @@ function install_zunclient {
|
|||||||
function install_zun {
|
function install_zun {
|
||||||
git_clone $ZUN_REPO $ZUN_DIR $ZUN_BRANCH
|
git_clone $ZUN_REPO $ZUN_DIR $ZUN_BRANCH
|
||||||
setup_develop $ZUN_DIR
|
setup_develop $ZUN_DIR
|
||||||
|
|
||||||
|
git_clone $ZUN_TEMPEST_PLUGIN_REPO $ZUN_TEMPEST_PLUGIN_DIR $ZUN_TEMPEST_PLUGIN_BRANCH
|
||||||
|
setup_develop $ZUN_TEMPEST_PLUGIN_DIR
|
||||||
}
|
}
|
||||||
|
|
||||||
function install_etcd_server {
|
function install_etcd_server {
|
||||||
|
@ -61,4 +61,4 @@ Navigate to tempest directory::
|
|||||||
|
|
||||||
Run this command::
|
Run this command::
|
||||||
|
|
||||||
tox -eall-plugin -- zun.tests.tempest.api
|
tox -eall-plugin -- zun_tempest_plugin.tests.tempest.api
|
||||||
|
@ -85,9 +85,6 @@ zun.network.driver =
|
|||||||
zun.volume.driver =
|
zun.volume.driver =
|
||||||
cinder = zun.volume.driver:Cinder
|
cinder = zun.volume.driver:Cinder
|
||||||
|
|
||||||
tempest.test_plugins =
|
|
||||||
zun_tests = zun.tests.tempest.plugin:ZunTempestPlugin
|
|
||||||
|
|
||||||
[extras]
|
[extras]
|
||||||
osprofiler =
|
osprofiler =
|
||||||
osprofiler>=1.4.0 # Apache-2.0
|
osprofiler>=1.4.0 # Apache-2.0
|
||||||
|
@ -27,7 +27,7 @@ export DEVSTACK_LOCAL_CONFIG+=$'\n'"ZUN_USE_UWSGI=True"
|
|||||||
export DEVSTACK_LOCAL_CONFIG+=$'\n'"KURYR_CONFIG_DIR=/etc/kuryr-libnetwork"
|
export DEVSTACK_LOCAL_CONFIG+=$'\n'"KURYR_CONFIG_DIR=/etc/kuryr-libnetwork"
|
||||||
export DEVSTACK_GATE_TEMPEST=1
|
export DEVSTACK_GATE_TEMPEST=1
|
||||||
export DEVSTACK_GATE_TEMPEST_ALL_PLUGINS=1
|
export DEVSTACK_GATE_TEMPEST_ALL_PLUGINS=1
|
||||||
export DEVSTACK_GATE_TEMPEST_REGEX="zun.tests.tempest.api"
|
export DEVSTACK_GATE_TEMPEST_REGEX="zun_tempest_plugin.tests.tempest.api"
|
||||||
|
|
||||||
if [ "$driver" = "docker" ]; then
|
if [ "$driver" = "docker" ]; then
|
||||||
export DEVSTACK_LOCAL_CONFIG+=$'\n'"ZUN_DRIVER=docker"
|
export DEVSTACK_LOCAL_CONFIG+=$'\n'"ZUN_DRIVER=docker"
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
==============
|
|
||||||
Tempest Plugin
|
|
||||||
==============
|
|
||||||
|
|
||||||
This directory contains Tempest tests to cover Zun project.
|
|
||||||
|
|
||||||
|
|
||||||
Tempest installation
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
To install Tempest you can issue the following commands::
|
|
||||||
|
|
||||||
$ git clone https://git.openstack.org/openstack/tempest/
|
|
||||||
$ cd tempest/
|
|
||||||
$ pip install .
|
|
||||||
|
|
||||||
The folder you are into now will be called ``<TEMPEST_DIR>`` from now onwards.
|
|
||||||
|
|
||||||
Please note that although it is fully working outside a virtual environment, it
|
|
||||||
is recommended to install within a `venv`.
|
|
||||||
|
|
||||||
Zun Tempest testing setup
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
Before using zun tempest plugin, you need to install zun first::
|
|
||||||
|
|
||||||
$ pip install -e <ZUN_SRC_DIR>
|
|
||||||
|
|
||||||
To list all Zun tempest cases, go to tempest directory, then run::
|
|
||||||
|
|
||||||
$ testr list-tests zun
|
|
||||||
|
|
||||||
Need to adopt tempest.conf, an example as follows::
|
|
||||||
|
|
||||||
$ cat /etc/tempest/tempest.conf
|
|
||||||
|
|
||||||
[auth]
|
|
||||||
use_dynamic_credentials=True
|
|
||||||
admin_username=admin
|
|
||||||
admin_password=123
|
|
||||||
admin_project_name=admin
|
|
||||||
|
|
||||||
[identity]
|
|
||||||
disable_ssl_certificate_validation=True
|
|
||||||
uri=http://127.0.0.1:5000/v2.0/
|
|
||||||
auth_version=v2
|
|
||||||
region=RegionOne
|
|
||||||
|
|
||||||
[identity-feature-enabled]
|
|
||||||
api_v2 = true
|
|
||||||
api_v3 = false
|
|
||||||
trust = false
|
|
||||||
|
|
||||||
[oslo_concurrency]
|
|
||||||
lock_path = /tmp/
|
|
||||||
|
|
||||||
[container_management]
|
|
||||||
catalog_type = container
|
|
||||||
|
|
||||||
[debug]
|
|
||||||
trace_requests=true
|
|
||||||
|
|
||||||
To run only these tests in tempest, go to tempest directory, then run::
|
|
||||||
|
|
||||||
$ tempest run zun
|
|
||||||
|
|
||||||
To run a single test case, go to tempest directory, then run with test case name, e.g.::
|
|
||||||
|
|
||||||
$ tempest run --regex zun.tests.tempest.api.test_containers.TestContainer.test_create_list_delete
|
|
@ -1,287 +0,0 @@
|
|||||||
# Licensed 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.
|
|
||||||
import contextlib
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from docker import errors as docker_errors
|
|
||||||
import six
|
|
||||||
from six.moves.urllib import parse
|
|
||||||
from tempest import config
|
|
||||||
from tempest.lib.common import rest_client
|
|
||||||
from tempest.lib.services.image.v2 import images_client
|
|
||||||
from tempest.lib.services.network import ports_client
|
|
||||||
from tempest.lib.services.network import security_groups_client
|
|
||||||
from tempest import manager
|
|
||||||
|
|
||||||
from zun.common import exception
|
|
||||||
import zun.conf
|
|
||||||
from zun.container.docker import utils as docker_utils
|
|
||||||
from zun.tests.tempest.api.models import container_model
|
|
||||||
from zun.tests.tempest.api.models import service_model
|
|
||||||
from zun.tests.tempest import utils
|
|
||||||
|
|
||||||
ZUN_CONF = zun.conf.CONF
|
|
||||||
CONF = config.CONF
|
|
||||||
|
|
||||||
|
|
||||||
class Manager(manager.Manager):
|
|
||||||
|
|
||||||
def __init__(self, credentials=None, service=None):
|
|
||||||
super(Manager, self).__init__(credentials=credentials)
|
|
||||||
|
|
||||||
self.images_client = images_client.ImagesClient(
|
|
||||||
self.auth_provider, 'image', CONF.identity.region)
|
|
||||||
self.ports_client = ports_client.PortsClient(
|
|
||||||
self.auth_provider, 'network', CONF.identity.region)
|
|
||||||
self.sgs_client = security_groups_client.SecurityGroupsClient(
|
|
||||||
self.auth_provider, 'network', CONF.identity.region)
|
|
||||||
self.container_client = ZunClient(self.auth_provider)
|
|
||||||
|
|
||||||
|
|
||||||
class ZunClient(rest_client.RestClient):
|
|
||||||
|
|
||||||
def __init__(self, auth_provider):
|
|
||||||
super(ZunClient, self).__init__(
|
|
||||||
auth_provider=auth_provider,
|
|
||||||
service=CONF.container_management.catalog_type,
|
|
||||||
region=CONF.identity.region,
|
|
||||||
disable_ssl_certificate_validation=True
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def deserialize(cls, resp, body, model_type):
|
|
||||||
return resp, model_type.from_json(body)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def containers_uri(cls, params=None):
|
|
||||||
url = "/containers/"
|
|
||||||
if params:
|
|
||||||
url = cls.add_params(url, params)
|
|
||||||
return url
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def container_uri(cls, container_id, action=None, params=None):
|
|
||||||
"""Construct container uri
|
|
||||||
|
|
||||||
"""
|
|
||||||
url = None
|
|
||||||
if action is None:
|
|
||||||
url = "{0}/{1}".format(cls.containers_uri(), container_id)
|
|
||||||
else:
|
|
||||||
url = "{0}/{1}/{2}".format(cls.containers_uri(), container_id,
|
|
||||||
action)
|
|
||||||
|
|
||||||
if params:
|
|
||||||
url = cls.add_params(url, params)
|
|
||||||
|
|
||||||
return url
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_params(cls, url, params):
|
|
||||||
"""add_params adds dict values (params) to url as query parameters
|
|
||||||
|
|
||||||
:param url: base URL for the request
|
|
||||||
:param params: dict with var:val pairs to add as parameters to URL
|
|
||||||
:returns: url string
|
|
||||||
"""
|
|
||||||
url_parts = list(parse.urlparse(url))
|
|
||||||
query = dict(parse.parse_qsl(url_parts[4]))
|
|
||||||
query.update(params)
|
|
||||||
url_parts[4] = parse.urlencode(query)
|
|
||||||
return parse.urlunparse(url_parts)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def services_uri(cls):
|
|
||||||
url = "/services/"
|
|
||||||
return url
|
|
||||||
|
|
||||||
def post_container(self, model, **kwargs):
|
|
||||||
"""Makes POST /container request
|
|
||||||
|
|
||||||
"""
|
|
||||||
resp, body = self.post(
|
|
||||||
self.containers_uri(),
|
|
||||||
body=model.to_json(), **kwargs)
|
|
||||||
return self.deserialize(resp, body, container_model.ContainerEntity)
|
|
||||||
|
|
||||||
def run_container(self, model, **kwargs):
|
|
||||||
resp, body = self.post(
|
|
||||||
self.containers_uri(params={'run': True}),
|
|
||||||
body=model.to_json(), **kwargs)
|
|
||||||
return self.deserialize(resp, body, container_model.ContainerEntity)
|
|
||||||
|
|
||||||
def get_container(self, container_id):
|
|
||||||
resp, body = self.get(self.container_uri(container_id))
|
|
||||||
return self.deserialize(resp, body, container_model.ContainerEntity)
|
|
||||||
|
|
||||||
def list_containers(self, **kwargs):
|
|
||||||
resp, body = self.get(self.containers_uri(), **kwargs)
|
|
||||||
return self.deserialize(resp, body,
|
|
||||||
container_model.ContainerCollection)
|
|
||||||
|
|
||||||
def delete_container(self, container_id, params=None, **kwargs):
|
|
||||||
return self.delete(
|
|
||||||
self.container_uri(container_id, params=params), **kwargs)
|
|
||||||
|
|
||||||
def commit_container(self, container_id, params=None, **kwargs):
|
|
||||||
return self.post(
|
|
||||||
self.container_uri(container_id, action='commit', params=params),
|
|
||||||
None, **kwargs)
|
|
||||||
|
|
||||||
def start_container(self, container_id, **kwargs):
|
|
||||||
return self.post(
|
|
||||||
self.container_uri(container_id, action='start'), None, **kwargs)
|
|
||||||
|
|
||||||
def stop_container(self, container_id, **kwargs):
|
|
||||||
return self.post(
|
|
||||||
self.container_uri(container_id, action='stop'), None, *kwargs)
|
|
||||||
|
|
||||||
def pause_container(self, container_id, **kwargs):
|
|
||||||
return self.post(
|
|
||||||
self.container_uri(container_id, action='pause'), None, **kwargs)
|
|
||||||
|
|
||||||
def unpause_container(self, container_id, **kwargs):
|
|
||||||
return self.post(
|
|
||||||
self.container_uri(container_id, action='unpause'), None, **kwargs)
|
|
||||||
|
|
||||||
def kill_container(self, container_id, **kwargs):
|
|
||||||
return self.post(
|
|
||||||
self.container_uri(container_id, action='kill'), None, **kwargs)
|
|
||||||
|
|
||||||
def reboot_container(self, container_id, **kwargs):
|
|
||||||
return self.post(
|
|
||||||
self.container_uri(container_id, action='reboot'), None, **kwargs)
|
|
||||||
|
|
||||||
def exec_container(self, container_id, command, **kwargs):
|
|
||||||
return self.post(
|
|
||||||
self.container_uri(container_id, action='execute'),
|
|
||||||
'{"command": "%s"}' % command, **kwargs)
|
|
||||||
|
|
||||||
def logs_container(self, container_id, **kwargs):
|
|
||||||
return self.get(
|
|
||||||
self.container_uri(container_id, action='logs'), None, **kwargs)
|
|
||||||
|
|
||||||
def update_container(self, container_id, model, **kwargs):
|
|
||||||
resp, body = self.patch(
|
|
||||||
self.container_uri(container_id), body=model.to_json(), **kwargs)
|
|
||||||
return self.deserialize(resp, body, container_model.ContainerEntity)
|
|
||||||
|
|
||||||
def rename_container(self, container_id, model, **kwargs):
|
|
||||||
resp, body = self.post(
|
|
||||||
self.container_uri(container_id, action='rename'),
|
|
||||||
body=model.to_json(), **kwargs)
|
|
||||||
return self.deserialize(resp, body, container_model.ContainerEntity)
|
|
||||||
|
|
||||||
def top_container(self, container_id, **kwargs):
|
|
||||||
return self.get(
|
|
||||||
self.container_uri(container_id, action='top'), None, **kwargs)
|
|
||||||
|
|
||||||
def stats_container(self, container_id, **kwargs):
|
|
||||||
return self.get(
|
|
||||||
self.container_uri(container_id, action='stats'), None, **kwargs)
|
|
||||||
|
|
||||||
def add_security_group(self, container_id, model, **kwargs):
|
|
||||||
return self.post(
|
|
||||||
self.container_uri(container_id, action='add_security_group'),
|
|
||||||
body=model.to_json(), **kwargs)
|
|
||||||
|
|
||||||
def list_services(self, **kwargs):
|
|
||||||
resp, body = self.get(self.services_uri(), **kwargs)
|
|
||||||
return self.deserialize(resp, body,
|
|
||||||
service_model.ServiceCollection)
|
|
||||||
|
|
||||||
def ensure_container_in_desired_state(self, container_id, status):
|
|
||||||
def is_container_in_desired_state():
|
|
||||||
_, container = self.get_container(container_id)
|
|
||||||
if container.status == status:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
utils.wait_for_condition(is_container_in_desired_state)
|
|
||||||
|
|
||||||
def ensure_container_deleted(self, container_id):
|
|
||||||
def is_container_deleted():
|
|
||||||
_, model = self.list_containers()
|
|
||||||
container_ids = [c['uuid'] for c in model.containers]
|
|
||||||
if container_id in container_ids:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
utils.wait_for_condition(is_container_deleted)
|
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def docker_client(docker_auth_url):
|
|
||||||
client_kwargs = dict()
|
|
||||||
if not ZUN_CONF.docker.api_insecure:
|
|
||||||
client_kwargs['ca_cert'] = CONF.docker.ca_file
|
|
||||||
client_kwargs['client_key'] = CONF.docker.key_file
|
|
||||||
client_kwargs['client_cert'] = CONF.docker.key_file
|
|
||||||
|
|
||||||
try:
|
|
||||||
yield docker_utils.DockerHTTPClient(
|
|
||||||
docker_auth_url,
|
|
||||||
ZUN_CONF.docker.docker_remote_api_version,
|
|
||||||
ZUN_CONF.docker.default_timeout,
|
|
||||||
**client_kwargs
|
|
||||||
)
|
|
||||||
except docker_errors.APIError as e:
|
|
||||||
desired_exc = exception.DockerError(error_msg=six.text_type(e))
|
|
||||||
six.reraise(type(desired_exc), desired_exc, sys.exc_info()[2])
|
|
||||||
|
|
||||||
|
|
||||||
class DockerClient(object):
|
|
||||||
|
|
||||||
def get_container(self, container_id,
|
|
||||||
docker_auth_url=ZUN_CONF.docker.api_url):
|
|
||||||
with docker_client(docker_auth_url) as docker:
|
|
||||||
for info in docker.list_instances(inspect=True):
|
|
||||||
if container_id in info['Name']:
|
|
||||||
return info
|
|
||||||
return None
|
|
||||||
|
|
||||||
def ensure_container_pid_changed(
|
|
||||||
self, container_id, pid,
|
|
||||||
docker_auth_url=ZUN_CONF.docker.api_url):
|
|
||||||
def is_pid_changed():
|
|
||||||
container = self.get_container(container_id,
|
|
||||||
docker_auth_url=docker_auth_url)
|
|
||||||
new_pid = container.get('State').get('Pid')
|
|
||||||
if pid != new_pid:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
utils.wait_for_condition(is_pid_changed)
|
|
||||||
|
|
||||||
def pull_image(
|
|
||||||
self, repo, tag=None,
|
|
||||||
docker_auth_url=ZUN_CONF.docker.api_url):
|
|
||||||
with docker_client(docker_auth_url) as docker:
|
|
||||||
docker.pull(repo, tag=tag)
|
|
||||||
|
|
||||||
def get_image(self, name, docker_auth_url=ZUN_CONF.docker.api_url):
|
|
||||||
with docker_client(docker_auth_url) as docker:
|
|
||||||
return docker.get_image(name)
|
|
||||||
|
|
||||||
def delete_image(self, name, docker_auth_url=ZUN_CONF.docker.api_url):
|
|
||||||
with docker_client(docker_auth_url) as docker:
|
|
||||||
return docker.remove_image(name)
|
|
||||||
|
|
||||||
def list_networks(self, name,
|
|
||||||
docker_auth_url=ZUN_CONF.docker.api_url):
|
|
||||||
with docker_client(docker_auth_url) as docker:
|
|
||||||
return docker.networks(names=[name])
|
|
||||||
|
|
||||||
def remove_network(self, name,
|
|
||||||
docker_auth_url=ZUN_CONF.docker.api_url):
|
|
||||||
with docker_client(docker_auth_url) as docker:
|
|
||||||
return docker.remove_network(name)
|
|
@ -1,70 +0,0 @@
|
|||||||
# Licensed 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 oslo_serialization import jsonutils as json
|
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(object):
|
|
||||||
"""Superclass responsible for converting json data to/from model"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_json(cls, json_str):
|
|
||||||
return cls.from_dict(json.loads(json_str))
|
|
||||||
|
|
||||||
def to_json(self):
|
|
||||||
return json.dump_as_bytes(self.to_dict())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data):
|
|
||||||
model = cls()
|
|
||||||
for key in data:
|
|
||||||
setattr(model, key, data.get(key))
|
|
||||||
return model
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
result = {}
|
|
||||||
for key in self.__dict__:
|
|
||||||
result[key] = getattr(self, key)
|
|
||||||
if isinstance(result[key], BaseModel):
|
|
||||||
result[key] = result[key].to_dict()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%s" % self.to_dict()
|
|
||||||
|
|
||||||
|
|
||||||
class EntityModel(BaseModel):
|
|
||||||
"""Superclass resposible from converting dict to instance of model"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data):
|
|
||||||
model = super(EntityModel, cls).from_dict(data)
|
|
||||||
if hasattr(model, cls.ENTITY_NAME):
|
|
||||||
val = getattr(model, cls.ENTITY_NAME)
|
|
||||||
setattr(model, cls.ENTITY_NAME, cls.MODEL_TYPE.from_dict(val))
|
|
||||||
return model
|
|
||||||
|
|
||||||
|
|
||||||
class CollectionModel(BaseModel):
|
|
||||||
"""Superclass resposible from converting dict to list of models"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data):
|
|
||||||
model = super(CollectionModel, cls).from_dict(data)
|
|
||||||
|
|
||||||
collection = []
|
|
||||||
if hasattr(model, cls.COLLECTION_NAME):
|
|
||||||
for d in getattr(model, cls.COLLECTION_NAME):
|
|
||||||
collection.append(cls.MODEL_TYPE.from_dict(d))
|
|
||||||
setattr(model, cls.COLLECTION_NAME, collection)
|
|
||||||
|
|
||||||
return model
|
|
@ -1,105 +0,0 @@
|
|||||||
# Licensed 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.
|
|
||||||
|
|
||||||
import random
|
|
||||||
import socket
|
|
||||||
import string
|
|
||||||
import struct
|
|
||||||
|
|
||||||
from tempest.lib.common.utils import data_utils
|
|
||||||
from zun.tests.tempest.api.models import container_model
|
|
||||||
|
|
||||||
|
|
||||||
def random_int(min_int=1, max_int=100):
|
|
||||||
return random.randrange(min_int, max_int)
|
|
||||||
|
|
||||||
|
|
||||||
def gen_random_port():
|
|
||||||
return random_int(49152, 65535)
|
|
||||||
|
|
||||||
|
|
||||||
def gen_docker_volume_size(min_int=3, max_int=5):
|
|
||||||
return random_int(min_int, max_int)
|
|
||||||
|
|
||||||
|
|
||||||
def gen_fake_ssh_pubkey():
|
|
||||||
chars = "".join(
|
|
||||||
random.choice(string.ascii_uppercase +
|
|
||||||
string.ascii_letters + string.digits + '/+=')
|
|
||||||
for _ in range(372))
|
|
||||||
return "ssh-rsa " + chars
|
|
||||||
|
|
||||||
|
|
||||||
def gen_random_ip():
|
|
||||||
return socket.inet_ntoa(struct.pack('>I', random.randint(1, 0xffffffff)))
|
|
||||||
|
|
||||||
|
|
||||||
def gen_url(scheme="http", domain="example.com", port=80):
|
|
||||||
return "%s://%s:%s" % (scheme, domain, port)
|
|
||||||
|
|
||||||
|
|
||||||
def container_data(default_data=None, **kwargs):
|
|
||||||
if default_data is None:
|
|
||||||
default_data = {
|
|
||||||
'name': data_utils.rand_name('container'),
|
|
||||||
'image': 'cirros:latest',
|
|
||||||
'command': 'sleep 10000',
|
|
||||||
'cpu': 0.1,
|
|
||||||
'memory': '100',
|
|
||||||
'environment': {},
|
|
||||||
'labels': {},
|
|
||||||
'image_driver': 'docker',
|
|
||||||
'image_pull_policy': 'always',
|
|
||||||
'restart_policy': {'Name': 'no'},
|
|
||||||
'workdir': '/',
|
|
||||||
'interactive': False,
|
|
||||||
'security_groups': ['default'],
|
|
||||||
}
|
|
||||||
|
|
||||||
default_data.update(kwargs)
|
|
||||||
model = container_model.ContainerEntity.from_dict(default_data)
|
|
||||||
|
|
||||||
return model
|
|
||||||
|
|
||||||
|
|
||||||
def container_patch_data(**kwargs):
|
|
||||||
data = {
|
|
||||||
'cpu': 0.2,
|
|
||||||
'memory': '512',
|
|
||||||
}
|
|
||||||
|
|
||||||
data.update(kwargs)
|
|
||||||
model = container_model.ContainerPatchEntity.from_dict(data)
|
|
||||||
|
|
||||||
return model
|
|
||||||
|
|
||||||
|
|
||||||
def container_rename_data(**kwargs):
|
|
||||||
data = {
|
|
||||||
'name': 'new_name',
|
|
||||||
}
|
|
||||||
|
|
||||||
data.update(kwargs)
|
|
||||||
model = container_model.ContainerPatchEntity.from_dict(data)
|
|
||||||
|
|
||||||
return model
|
|
||||||
|
|
||||||
|
|
||||||
def container_add_sg_data(**kwargs):
|
|
||||||
data = {
|
|
||||||
'name': 'sg_name',
|
|
||||||
}
|
|
||||||
|
|
||||||
data.update(kwargs)
|
|
||||||
model = container_model.ContainerPatchEntity.from_dict(data)
|
|
||||||
|
|
||||||
return model
|
|
@ -1,41 +0,0 @@
|
|||||||
# Licensed 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 zun.tests.tempest.api.common import base_model
|
|
||||||
|
|
||||||
|
|
||||||
class ContainerData(base_model.BaseModel):
|
|
||||||
"""Data that encapsulates container attributes"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ContainerEntity(base_model.EntityModel):
|
|
||||||
"""Entity Model that represents a single instance of ContainerData"""
|
|
||||||
ENTITY_NAME = 'container'
|
|
||||||
MODEL_TYPE = ContainerData
|
|
||||||
|
|
||||||
|
|
||||||
class ContainerCollection(base_model.CollectionModel):
|
|
||||||
"""Collection Model that represents a list of ContainerData objects"""
|
|
||||||
COLLECTION_NAME = 'containerlists'
|
|
||||||
MODEL_TYPE = ContainerData
|
|
||||||
|
|
||||||
|
|
||||||
class ContainerPatchData(base_model.BaseModel):
|
|
||||||
"""Data that encapsulates container update attributes"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ContainerPatchEntity(base_model.EntityModel):
|
|
||||||
"""Entity Model that represents a single instance of ContainerPatchData"""
|
|
||||||
ENTITY_NAME = 'containerpatch'
|
|
||||||
MODEL_TYPE = ContainerPatchData
|
|
@ -1,30 +0,0 @@
|
|||||||
# Licensed 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 zun.tests.tempest.api.common import base_model
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceData(base_model.BaseModel):
|
|
||||||
"""Data that encapsulates service attributes"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceEntity(base_model.EntityModel):
|
|
||||||
"""Entity Model that represents a single instance of ServiceData"""
|
|
||||||
ENTITY_NAME = 'service'
|
|
||||||
MODEL_TYPE = ServiceData
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceCollection(base_model.CollectionModel):
|
|
||||||
"""Collection Model that represents a list of ServiceData objects"""
|
|
||||||
COLLECTION_NAME = 'servicelists'
|
|
||||||
MODEL_TYPE = ServiceData
|
|
@ -1,511 +0,0 @@
|
|||||||
# Licensed 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 oslo_utils import encodeutils
|
|
||||||
from tempest.lib import decorators
|
|
||||||
|
|
||||||
from zun.tests.tempest.api import clients
|
|
||||||
from zun.tests.tempest.api.common import datagen
|
|
||||||
from zun.tests.tempest import base
|
|
||||||
from zun.tests.tempest import utils
|
|
||||||
|
|
||||||
|
|
||||||
class TestContainer(base.BaseZunTest):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_client_manager(cls, credential_type=None, roles=None,
|
|
||||||
force_new=None):
|
|
||||||
manager = super(TestContainer, cls).get_client_manager(
|
|
||||||
credential_type=credential_type,
|
|
||||||
roles=roles,
|
|
||||||
force_new=force_new
|
|
||||||
)
|
|
||||||
return clients.Manager(manager.credentials)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setup_clients(cls):
|
|
||||||
super(TestContainer, cls).setup_clients()
|
|
||||||
cls.container_client = cls.os_primary.container_client
|
|
||||||
cls.docker_client = clients.DockerClient()
|
|
||||||
cls.images_client = cls.os_primary.images_client
|
|
||||||
cls.ports_client = cls.os_primary.ports_client
|
|
||||||
cls.sgs_client = cls.os_primary.sgs_client
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def resource_setup(cls):
|
|
||||||
super(TestContainer, cls).resource_setup()
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestContainer, self).setUp()
|
|
||||||
self.containers = []
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
hosts = []
|
|
||||||
_, model = self.container_client.list_containers()
|
|
||||||
for c in model.containers:
|
|
||||||
if c['uuid'] in self.containers:
|
|
||||||
if c['host'] and c['host'] not in hosts:
|
|
||||||
hosts.append(c['host'])
|
|
||||||
self.container_client.delete_container(c['uuid'],
|
|
||||||
params={'force': True})
|
|
||||||
self.container_client.ensure_container_deleted(c['uuid'])
|
|
||||||
|
|
||||||
# cleanup the network resources
|
|
||||||
project_id = self.container_client.tenant_id
|
|
||||||
for host in hosts:
|
|
||||||
# NOTE(kiennt): Default docker remote url
|
|
||||||
# Remove networks in all hosts
|
|
||||||
docker_base_url = self._get_docker_url(host)
|
|
||||||
networks = self.docker_client.list_networks(project_id,
|
|
||||||
docker_base_url)
|
|
||||||
for network in networks:
|
|
||||||
self.docker_client.remove_network(network['Id'],
|
|
||||||
docker_base_url)
|
|
||||||
|
|
||||||
super(TestContainer, self).tearDown()
|
|
||||||
|
|
||||||
@decorators.idempotent_id('b8946b8c-57d5-4fdc-a09a-001d6b552725')
|
|
||||||
def test_create_container(self):
|
|
||||||
self._create_container()
|
|
||||||
|
|
||||||
@decorators.idempotent_id('b3e307d4-844b-4a57-8c60-8fb3f57aea7c')
|
|
||||||
def test_list_containers(self):
|
|
||||||
_, container = self._create_container()
|
|
||||||
resp, model = self.container_client.list_containers()
|
|
||||||
self.assertEqual(200, resp.status)
|
|
||||||
self.assertGreater(len(model.containers), 0)
|
|
||||||
self.assertIn(
|
|
||||||
container.uuid,
|
|
||||||
list([c['uuid'] for c in model.containers]))
|
|
||||||
|
|
||||||
@decorators.idempotent_id('0dd13c28-c5ff-4b9e-b73b-61185b410de4')
|
|
||||||
def test_get_container(self):
|
|
||||||
_, container = self._create_container()
|
|
||||||
resp, model = self.container_client.get_container(container.uuid)
|
|
||||||
self.assertEqual(200, resp.status)
|
|
||||||
self.assertEqual(container.uuid, model.uuid)
|
|
||||||
|
|
||||||
@decorators.idempotent_id('cef53a56-22b7-4808-b01c-06b2b7126115')
|
|
||||||
def test_delete_container(self):
|
|
||||||
_, container = self._create_container()
|
|
||||||
self._delete_container(container.uuid, container.host, True)
|
|
||||||
|
|
||||||
@decorators.idempotent_id('ef69c9e7-0ce0-4e14-b7ec-c1dc581a3927')
|
|
||||||
def test_run_container(self):
|
|
||||||
self._run_container()
|
|
||||||
|
|
||||||
@decorators.idempotent_id('a2152d78-b6a6-4f47-8767-d83d29c6fb19')
|
|
||||||
def test_run_container_with_minimal_params(self):
|
|
||||||
gen_model = datagen.container_data({'image': 'nginx'})
|
|
||||||
self._run_container(gen_model=gen_model)
|
|
||||||
|
|
||||||
@decorators.idempotent_id('c32f93e3-da88-4c13-be38-25d2e662a28e')
|
|
||||||
def test_run_container_with_image_driver_glance(self):
|
|
||||||
image = None
|
|
||||||
try:
|
|
||||||
docker_base_url = self._get_docker_url()
|
|
||||||
self.docker_client.pull_image(
|
|
||||||
'cirros', docker_auth_url=docker_base_url)
|
|
||||||
image_data = self.docker_client.get_image(
|
|
||||||
'cirros', docker_base_url)
|
|
||||||
image = self.images_client.create_image(
|
|
||||||
name='cirros', disk_format='raw', container_format='docker')
|
|
||||||
self.images_client.store_image_file(image['id'], image_data)
|
|
||||||
# delete the local image that was previously pulled down
|
|
||||||
self.docker_client.delete_image('cirros', docker_base_url)
|
|
||||||
|
|
||||||
_, model = self._run_container(
|
|
||||||
image='cirros', image_driver='glance')
|
|
||||||
finally:
|
|
||||||
if image:
|
|
||||||
try:
|
|
||||||
self.images_client.delete_image(image['id'])
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@decorators.idempotent_id('b70bedbc-5ba2-400c-8f5f-0cf05ca17151')
|
|
||||||
def test_run_container_with_environment(self):
|
|
||||||
_, model = self._run_container(environment={
|
|
||||||
'key1': 'env1', 'key2': 'env2'})
|
|
||||||
|
|
||||||
container = self.docker_client.get_container(
|
|
||||||
model.uuid,
|
|
||||||
self._get_docker_url(model.host))
|
|
||||||
env = container.get('Config').get('Env')
|
|
||||||
self.assertTrue('key1=env1', env)
|
|
||||||
self.assertTrue('key2=env2', env)
|
|
||||||
|
|
||||||
@decorators.idempotent_id('0e59d549-58ff-440f-8704-10e223c31cbc')
|
|
||||||
def test_run_container_with_labels(self):
|
|
||||||
_, model = self._run_container(labels={
|
|
||||||
'key1': 'label1', 'key2': 'label2'})
|
|
||||||
|
|
||||||
container = self.docker_client.get_container(
|
|
||||||
model.uuid,
|
|
||||||
self._get_docker_url(model.host))
|
|
||||||
labels = container.get('Config').get('Labels')
|
|
||||||
self.assertTrue('key1=label1', labels)
|
|
||||||
self.assertTrue('key2=label2', labels)
|
|
||||||
|
|
||||||
@decorators.idempotent_id('9fc7fec0-e1a9-4f65-a5a6-dba425c1607c')
|
|
||||||
def test_run_container_with_restart_policy(self):
|
|
||||||
_, model = self._run_container(restart_policy={
|
|
||||||
'Name': 'on-failure', 'MaximumRetryCount': 2})
|
|
||||||
|
|
||||||
container = self.docker_client.get_container(
|
|
||||||
model.uuid,
|
|
||||||
self._get_docker_url(model.host))
|
|
||||||
policy = container.get('HostConfig').get('RestartPolicy')
|
|
||||||
self.assertEqual('on-failure', policy['Name'])
|
|
||||||
self.assertTrue(2, policy['MaximumRetryCount'])
|
|
||||||
|
|
||||||
@decorators.idempotent_id('58585a4f-cdce-4dbd-9741-4416d1098f94')
|
|
||||||
def test_run_container_with_interactive(self):
|
|
||||||
_, model = self._run_container(interactive=True)
|
|
||||||
|
|
||||||
container = self.docker_client.get_container(
|
|
||||||
model.uuid,
|
|
||||||
self._get_docker_url(model.host))
|
|
||||||
tty = container.get('Config').get('Tty')
|
|
||||||
stdin_open = container.get('Config').get('OpenStdin')
|
|
||||||
self.assertIs(True, tty)
|
|
||||||
self.assertIs(True, stdin_open)
|
|
||||||
|
|
||||||
@decorators.idempotent_id('f181eeda-a9d1-4b2e-9746-d6634ca81e2f')
|
|
||||||
def test_run_container_without_security_groups(self):
|
|
||||||
gen_model = datagen.container_data()
|
|
||||||
delattr(gen_model, 'security_groups')
|
|
||||||
_, model = self._run_container(gen_model=gen_model)
|
|
||||||
sgs = self._get_all_security_groups(model)
|
|
||||||
self.assertEqual(1, len(sgs))
|
|
||||||
self.assertEqual('default', sgs[0])
|
|
||||||
|
|
||||||
@decorators.idempotent_id('f181eeda-a9d1-4b2e-9746-d6634ca81e2f')
|
|
||||||
def test_run_container_with_security_groups(self):
|
|
||||||
sg_name = 'test_sg'
|
|
||||||
self.sgs_client.create_security_group(name=sg_name)
|
|
||||||
_, model = self._run_container(security_groups=[sg_name])
|
|
||||||
sgs = self._get_all_security_groups(model)
|
|
||||||
self.assertEqual(1, len(sgs))
|
|
||||||
self.assertEqual(sg_name, sgs[0])
|
|
||||||
|
|
||||||
@decorators.idempotent_id('c3f02fa0-fdfb-49fc-95e2-6e4dc982f9be')
|
|
||||||
def test_commit_container(self):
|
|
||||||
"""Test container snapshot
|
|
||||||
|
|
||||||
This test does the following:
|
|
||||||
1. Create a container
|
|
||||||
2. Create and write to a file inside the container
|
|
||||||
3. Commit the container and upload the snapshot to Glance
|
|
||||||
4. Create another container from the snapshot image
|
|
||||||
5. Verify the pre-created file is there
|
|
||||||
"""
|
|
||||||
# This command creates a file inside the container
|
|
||||||
command = "/bin/sh -c 'echo hello > testfile;sleep 1000000'"
|
|
||||||
_, model = self._run_container(command=command)
|
|
||||||
|
|
||||||
try:
|
|
||||||
resp, _ = self.container_client.commit_container(
|
|
||||||
model.uuid, params={'repository': 'myrepo'})
|
|
||||||
self.assertEqual(202, resp.status)
|
|
||||||
self._ensure_image_active('myrepo')
|
|
||||||
|
|
||||||
# This command outputs the content of pre-created file
|
|
||||||
command = "/bin/sh -c 'cat testfile;sleep 1000000'"
|
|
||||||
_, model = self._run_container(
|
|
||||||
image="myrepo", image_driver="glance", command=command)
|
|
||||||
resp, body = self.container_client.logs_container(model.uuid)
|
|
||||||
self.assertEqual(200, resp.status)
|
|
||||||
self.assertTrue('hello' in encodeutils.safe_decode(body))
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
response = self.images_client.list_images()
|
|
||||||
for image in response['images']:
|
|
||||||
if (image['name'] == 'myrepo' and
|
|
||||||
image['container_format'] == 'docker'):
|
|
||||||
self.images_client.delete_image(image['id'])
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _ensure_image_active(self, image_name):
|
|
||||||
def is_image_in_desired_state():
|
|
||||||
response = self.images_client.list_images()
|
|
||||||
for image in response['images']:
|
|
||||||
if (image['name'] == image_name and
|
|
||||||
image['container_format'] == 'docker' and
|
|
||||||
image['status'] == 'active'):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
utils.wait_for_condition(is_image_in_desired_state)
|
|
||||||
|
|
||||||
@decorators.idempotent_id('3fa024ef-aba1-48fe-9682-0d6b7854faa3')
|
|
||||||
def test_start_stop_container(self):
|
|
||||||
_, model = self._run_container()
|
|
||||||
|
|
||||||
resp, _ = self.container_client.stop_container(model.uuid)
|
|
||||||
self.assertEqual(202, resp.status)
|
|
||||||
self.container_client.ensure_container_in_desired_state(
|
|
||||||
model.uuid, 'Stopped')
|
|
||||||
self.assertEqual('Stopped',
|
|
||||||
self._get_container_state(model.uuid, model.host))
|
|
||||||
|
|
||||||
resp, _ = self.container_client.start_container(model.uuid)
|
|
||||||
self.assertEqual(202, resp.status)
|
|
||||||
self.container_client.ensure_container_in_desired_state(
|
|
||||||
model.uuid, 'Running')
|
|
||||||
self.assertEqual('Running',
|
|
||||||
self._get_container_state(model.uuid, model.host))
|
|
||||||
|
|
||||||
@decorators.idempotent_id('b5f39756-8898-4e0e-a48b-dda0a06b66b6')
|
|
||||||
def test_pause_unpause_container(self):
|
|
||||||
_, model = self._run_container()
|
|
||||||
|
|
||||||
resp, _ = self.container_client.pause_container(model.uuid)
|
|
||||||
self.assertEqual(202, resp.status)
|
|
||||||
self.container_client.ensure_container_in_desired_state(
|
|
||||||
model.uuid, 'Paused')
|
|
||||||
self.assertEqual('Paused',
|
|
||||||
self._get_container_state(model.uuid, model.host))
|
|
||||||
|
|
||||||
resp, _ = self.container_client.unpause_container(model.uuid)
|
|
||||||
self.assertEqual(202, resp.status)
|
|
||||||
self.container_client.ensure_container_in_desired_state(
|
|
||||||
model.uuid, 'Running')
|
|
||||||
self.assertEqual('Running',
|
|
||||||
self._get_container_state(model.uuid, model.host))
|
|
||||||
|
|
||||||
@decorators.idempotent_id('6179a588-3d48-4372-9599-f228411d1449')
|
|
||||||
def test_kill_container(self):
|
|
||||||
_, model = self._run_container()
|
|
||||||
|
|
||||||
resp, _ = self.container_client.kill_container(model.uuid)
|
|
||||||
self.assertEqual(202, resp.status)
|
|
||||||
self.container_client.ensure_container_in_desired_state(
|
|
||||||
model.uuid, 'Stopped')
|
|
||||||
self.assertEqual('Stopped',
|
|
||||||
self._get_container_state(model.uuid, model.host))
|
|
||||||
|
|
||||||
@decorators.idempotent_id('c2e54321-0a70-4331-ba62-9dcaa75ac250')
|
|
||||||
def test_reboot_container(self):
|
|
||||||
_, model = self._run_container()
|
|
||||||
docker_base_url = self._get_docker_url(model.host)
|
|
||||||
container = self.docker_client.get_container(model.uuid,
|
|
||||||
docker_base_url)
|
|
||||||
pid = container.get('State').get('Pid')
|
|
||||||
|
|
||||||
resp, _ = self.container_client.reboot_container(model.uuid)
|
|
||||||
self.assertEqual(202, resp.status)
|
|
||||||
self.docker_client.ensure_container_pid_changed(model.uuid, pid,
|
|
||||||
docker_base_url)
|
|
||||||
self.assertEqual('Running',
|
|
||||||
self._get_container_state(model.uuid, model.host))
|
|
||||||
# assert pid is changed
|
|
||||||
container = self.docker_client.get_container(model.uuid,
|
|
||||||
docker_base_url)
|
|
||||||
self.assertNotEqual(pid, container.get('State').get('Pid'))
|
|
||||||
|
|
||||||
@decorators.idempotent_id('8a591ff8-6793-427f-82a6-e3921d8b4f81')
|
|
||||||
def test_exec_container(self):
|
|
||||||
_, model = self._run_container()
|
|
||||||
resp, body = self.container_client.exec_container(model.uuid,
|
|
||||||
command='echo hello')
|
|
||||||
self.assertEqual(200, resp.status)
|
|
||||||
self.assertTrue('hello' in encodeutils.safe_decode(body))
|
|
||||||
|
|
||||||
@decorators.idempotent_id('a912ca23-14e7-442f-ab15-e05aaa315204')
|
|
||||||
def test_logs_container(self):
|
|
||||||
_, model = self._run_container(
|
|
||||||
command="/bin/sh -c 'echo hello;sleep 1000000'")
|
|
||||||
resp, body = self.container_client.logs_container(model.uuid)
|
|
||||||
self.assertEqual(200, resp.status)
|
|
||||||
self.assertTrue('hello' in encodeutils.safe_decode(body))
|
|
||||||
|
|
||||||
@decorators.idempotent_id('d383f359-3ebd-40ef-9dc5-d36922790230')
|
|
||||||
def test_update_container(self):
|
|
||||||
_, model = self._run_container(cpu=0.1, memory=100)
|
|
||||||
self.assertEqual('100M', model.memory)
|
|
||||||
self.assertEqual(0.1, model.cpu)
|
|
||||||
docker_base_url = self._get_docker_url(model.host)
|
|
||||||
container = self.docker_client.get_container(model.uuid,
|
|
||||||
docker_base_url)
|
|
||||||
self._assert_resource_constraints(container, cpu=0.1, memory=100)
|
|
||||||
|
|
||||||
gen_model = datagen.container_patch_data(cpu=0.2, memory=200)
|
|
||||||
resp, model = self.container_client.update_container(model.uuid,
|
|
||||||
gen_model)
|
|
||||||
self.assertEqual(200, resp.status)
|
|
||||||
self.assertEqual('200M', model.memory)
|
|
||||||
self.assertEqual(0.2, model.cpu)
|
|
||||||
container = self.docker_client.get_container(model.uuid,
|
|
||||||
docker_base_url)
|
|
||||||
self._assert_resource_constraints(container, cpu=0.2, memory=200)
|
|
||||||
|
|
||||||
@decorators.idempotent_id('b218bea7-f19b-499f-9819-c7021ffc59f4')
|
|
||||||
def test_rename_container(self):
|
|
||||||
_, model = self._run_container(name='container1')
|
|
||||||
self.assertEqual('container1', model.name)
|
|
||||||
gen_model = datagen.container_rename_data(name='container2')
|
|
||||||
resp, model = self.container_client.rename_container(model.uuid,
|
|
||||||
gen_model)
|
|
||||||
self.assertEqual(200, resp.status)
|
|
||||||
self.assertEqual('container2', model.name)
|
|
||||||
|
|
||||||
@decorators.idempotent_id('142b7716-0b21-41ed-b47d-a42fba75636b')
|
|
||||||
def test_top_container(self):
|
|
||||||
_, model = self._run_container(
|
|
||||||
command="/bin/sh -c 'sleep 1000000'")
|
|
||||||
resp, body = self.container_client.top_container(model.uuid)
|
|
||||||
self.assertEqual(200, resp.status)
|
|
||||||
self.assertTrue('sleep 1000000' in encodeutils.safe_decode(body))
|
|
||||||
|
|
||||||
@decorators.idempotent_id('09638306-b501-4803-aafa-7e8025632cef')
|
|
||||||
def test_stats_container(self):
|
|
||||||
_, model = self._run_container()
|
|
||||||
resp, body = self.container_client.stats_container(model.uuid)
|
|
||||||
self.assertEqual(200, resp.status)
|
|
||||||
self.assertTrue('NET I/O(B)' in encodeutils.safe_decode(body))
|
|
||||||
self.assertTrue('CONTAINER' in encodeutils.safe_decode(body))
|
|
||||||
self.assertTrue('MEM LIMIT(MiB)' in encodeutils.safe_decode(body))
|
|
||||||
self.assertTrue('CPU %' in encodeutils.safe_decode(body))
|
|
||||||
self.assertTrue('MEM USAGE(MiB)' in encodeutils.safe_decode(body))
|
|
||||||
self.assertTrue('MEM %' in encodeutils.safe_decode(body))
|
|
||||||
self.assertTrue('BLOCK I/O(B)' in encodeutils.safe_decode(body))
|
|
||||||
|
|
||||||
@decorators.idempotent_id('b3b9cf17-82ad-4c1b-a4af-8210a778a33e')
|
|
||||||
def test_add_sg_to_container(self):
|
|
||||||
_, model = self._run_container()
|
|
||||||
sgs = self._get_all_security_groups(model)
|
|
||||||
self.assertEqual(1, len(sgs))
|
|
||||||
self.assertEqual('default', sgs[0])
|
|
||||||
|
|
||||||
sg_name = 'test_add_sg'
|
|
||||||
self.sgs_client.create_security_group(name=sg_name)
|
|
||||||
gen_model = datagen.container_add_sg_data(name=sg_name)
|
|
||||||
resp, body = self.container_client.add_security_group(
|
|
||||||
model.uuid, gen_model)
|
|
||||||
self.assertEqual(202, resp.status)
|
|
||||||
|
|
||||||
def assert_security_group_is_added():
|
|
||||||
sgs = self._get_all_security_groups(model)
|
|
||||||
if len(sgs) == 2:
|
|
||||||
self.assertTrue('default' in sgs)
|
|
||||||
self.assertTrue(sg_name in sgs)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
utils.wait_for_condition(assert_security_group_is_added)
|
|
||||||
|
|
||||||
def _assert_resource_constraints(self, container, cpu=None, memory=None):
|
|
||||||
if cpu is not None:
|
|
||||||
cpu_quota = container.get('HostConfig').get('CpuQuota')
|
|
||||||
self.assertEqual(int(cpu * 100000), cpu_quota)
|
|
||||||
cpu_period = container.get('HostConfig').get('CpuPeriod')
|
|
||||||
self.assertEqual(100000, cpu_period)
|
|
||||||
if memory is not None:
|
|
||||||
docker_memory = container.get('HostConfig').get('Memory')
|
|
||||||
self.assertEqual(memory * 1024 * 1024, docker_memory)
|
|
||||||
|
|
||||||
def _create_container(self, **kwargs):
|
|
||||||
gen_model = datagen.container_data(**kwargs)
|
|
||||||
resp, model = self.container_client.post_container(gen_model)
|
|
||||||
self.containers.append(model.uuid)
|
|
||||||
self.assertEqual(202, resp.status)
|
|
||||||
# Wait for container to finish creation
|
|
||||||
self.container_client.ensure_container_in_desired_state(
|
|
||||||
model.uuid, 'Created')
|
|
||||||
|
|
||||||
# Assert the container is created
|
|
||||||
resp, model = self.container_client.get_container(model.uuid)
|
|
||||||
self.assertEqual(200, resp.status)
|
|
||||||
self.assertEqual('Created', model.status)
|
|
||||||
self.assertEqual('Created', self._get_container_state(model.uuid,
|
|
||||||
model.host))
|
|
||||||
return resp, model
|
|
||||||
|
|
||||||
def _run_container(self, gen_model=None, **kwargs):
|
|
||||||
if gen_model is None:
|
|
||||||
gen_model = datagen.container_data(**kwargs)
|
|
||||||
resp, model = self.container_client.run_container(gen_model)
|
|
||||||
self.containers.append(model.uuid)
|
|
||||||
self.assertEqual(202, resp.status)
|
|
||||||
# Wait for container to started
|
|
||||||
self.container_client.ensure_container_in_desired_state(
|
|
||||||
model.uuid, 'Running')
|
|
||||||
|
|
||||||
# Assert the container is started
|
|
||||||
resp, model = self.container_client.get_container(model.uuid)
|
|
||||||
self.assertEqual('Running', model.status)
|
|
||||||
self.assertEqual('Running', self._get_container_state(model.uuid,
|
|
||||||
model.host))
|
|
||||||
self.assertIsNotNone(model.host)
|
|
||||||
return resp, model
|
|
||||||
|
|
||||||
def _delete_container(self, container_id, container_host, force=False):
|
|
||||||
resp, _ = self.container_client.delete_container(
|
|
||||||
container_id, params={'force': force})
|
|
||||||
self.assertEqual(204, resp.status)
|
|
||||||
self.container_client.ensure_container_deleted(container_id)
|
|
||||||
container = self.docker_client.get_container(
|
|
||||||
container_id, self._get_docker_url(container_host))
|
|
||||||
self.assertIsNone(container)
|
|
||||||
|
|
||||||
def _get_container_state(self, container_id, docker_host=None):
|
|
||||||
if docker_host is not None:
|
|
||||||
container = self.docker_client.get_container(
|
|
||||||
container_id, self._get_docker_url(docker_host))
|
|
||||||
else:
|
|
||||||
container = self.docker_client.get_container(container_id)
|
|
||||||
status = container.get('State')
|
|
||||||
if status.get('Error') is True:
|
|
||||||
return 'Error'
|
|
||||||
elif status.get('Paused'):
|
|
||||||
return 'Paused'
|
|
||||||
elif status.get('Running'):
|
|
||||||
return 'Running'
|
|
||||||
elif status.get('Status') == 'created':
|
|
||||||
return 'Created'
|
|
||||||
else:
|
|
||||||
return 'Stopped'
|
|
||||||
|
|
||||||
def _get_all_security_groups(self, container):
|
|
||||||
# find all neutron ports of this container
|
|
||||||
port_ids = set()
|
|
||||||
for addrs_list in container.addresses.values():
|
|
||||||
for addr in addrs_list:
|
|
||||||
port_id = addr['port']
|
|
||||||
port_ids.add(port_id)
|
|
||||||
|
|
||||||
# find all security groups of this container
|
|
||||||
sg_ids = set()
|
|
||||||
for port_id in port_ids:
|
|
||||||
port = self.ports_client.show_port(port_id)
|
|
||||||
for sg in port['port']['security_groups']:
|
|
||||||
sg_ids.add(sg)
|
|
||||||
|
|
||||||
sg_names = []
|
|
||||||
for sg_id in sg_ids:
|
|
||||||
sg = self.sgs_client.show_security_group(sg_id)
|
|
||||||
sg_names.append(sg['security_group']['name'])
|
|
||||||
|
|
||||||
return sg_names
|
|
||||||
|
|
||||||
def _get_docker_url(self, host='localhost', protocol='tcp', port='2375'):
|
|
||||||
# NOTE(kiennt): By default, devstack-plugin-container will
|
|
||||||
# set docker_api_url = {
|
|
||||||
# "unix://$DOCKER_ENGINE_SOCKET_FILE",
|
|
||||||
# "tcp://0.0.0.0:$DOCKER_ENGINE_PORT"
|
|
||||||
# }
|
|
||||||
base_url = '{}://{}:{}' . format(protocol, host, port)
|
|
||||||
return base_url
|
|
@ -1,50 +0,0 @@
|
|||||||
# Licensed 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 tempest.lib import decorators
|
|
||||||
from tempest.lib import exceptions
|
|
||||||
|
|
||||||
from zun.tests.tempest.api import clients
|
|
||||||
from zun.tests.tempest import base
|
|
||||||
|
|
||||||
|
|
||||||
class TestService(base.BaseZunTest):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_client_manager(cls, credential_type=None, roles=None,
|
|
||||||
force_new=None):
|
|
||||||
|
|
||||||
manager = super(TestService, cls).get_client_manager(
|
|
||||||
credential_type=credential_type,
|
|
||||||
roles=roles,
|
|
||||||
force_new=force_new
|
|
||||||
)
|
|
||||||
return clients.Manager(manager.credentials)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setup_clients(cls):
|
|
||||||
|
|
||||||
super(TestService, cls).setup_clients()
|
|
||||||
cls.container_client = cls.os_primary.container_client
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def resource_setup(cls):
|
|
||||||
|
|
||||||
super(TestService, cls).resource_setup()
|
|
||||||
|
|
||||||
# TODO(pksingh): currently functional test doesn't support
|
|
||||||
# policy, will write another test after
|
|
||||||
# implementing policy in functional tests
|
|
||||||
@decorators.idempotent_id('a04f61f2-15ae-4200-83b7-1f311b101f36')
|
|
||||||
def test_service_list(self):
|
|
||||||
self.assertRaises(exceptions.Forbidden,
|
|
||||||
self.container_client.list_services)
|
|
@ -1,35 +0,0 @@
|
|||||||
#
|
|
||||||
# Licensed 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 tempest import config
|
|
||||||
from tempest import test
|
|
||||||
|
|
||||||
CONF = config.CONF
|
|
||||||
|
|
||||||
|
|
||||||
class BaseZunTest(test.BaseTestCase):
|
|
||||||
|
|
||||||
credentials = ['primary']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def skip_checks(cls):
|
|
||||||
super(BaseZunTest, cls).skip_checks()
|
|
||||||
if not CONF.service_available.zun:
|
|
||||||
skip_msg = 'Zun is disabled'
|
|
||||||
raise cls.skipException(skip_msg)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setup_clients(cls):
|
|
||||||
super(BaseZunTest, cls).setup_clients()
|
|
||||||
pass
|
|
@ -1,31 +0,0 @@
|
|||||||
#
|
|
||||||
# Licensed 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 oslo_config import cfg
|
|
||||||
|
|
||||||
service_option = cfg.BoolOpt("zun",
|
|
||||||
default=True,
|
|
||||||
help="Whether or not zun is expected to be "
|
|
||||||
"available")
|
|
||||||
|
|
||||||
container_management_group = cfg.OptGroup(
|
|
||||||
name="container_management", title="Container Management Service Options")
|
|
||||||
|
|
||||||
ContainerManagementGroup = [
|
|
||||||
cfg.StrOpt("catalog_type",
|
|
||||||
default="container",
|
|
||||||
help="Catalog type of the container management service."),
|
|
||||||
cfg.IntOpt("wait_timeout",
|
|
||||||
default=60,
|
|
||||||
help="Waiting time for a specific status, in seconds.")
|
|
||||||
]
|
|
@ -1,41 +0,0 @@
|
|||||||
#
|
|
||||||
# Licensed 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.
|
|
||||||
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from tempest.test_discover import plugins
|
|
||||||
|
|
||||||
from zun.tests.tempest import config as config_zun
|
|
||||||
|
|
||||||
|
|
||||||
class ZunTempestPlugin(plugins.TempestPlugin):
|
|
||||||
def load_tests(self):
|
|
||||||
base_path = os.path.split(os.path.dirname(
|
|
||||||
os.path.abspath(__file__)))[0]
|
|
||||||
base_path += '/../..'
|
|
||||||
test_dir = "zun/tests/tempest"
|
|
||||||
full_test_dir = os.path.join(base_path, test_dir)
|
|
||||||
return full_test_dir, base_path
|
|
||||||
|
|
||||||
def register_opts(self, conf):
|
|
||||||
conf.register_opt(config_zun.service_option,
|
|
||||||
group='service_available')
|
|
||||||
conf.register_group(config_zun.container_management_group)
|
|
||||||
conf.register_opts(config_zun.ContainerManagementGroup,
|
|
||||||
group='container_management')
|
|
||||||
|
|
||||||
def get_opt_lists(self):
|
|
||||||
return [(config_zun.container_management_group.name,
|
|
||||||
config_zun.ContainerManagementGroup),
|
|
||||||
('service_available', [config_zun.service_option])]
|
|
@ -1,25 +0,0 @@
|
|||||||
# Licensed 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.
|
|
||||||
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
def wait_for_condition(condition, interval=2, timeout=60):
|
|
||||||
start_time = time.time()
|
|
||||||
end_time = time.time() + timeout
|
|
||||||
while time.time() < end_time:
|
|
||||||
result = condition()
|
|
||||||
if result:
|
|
||||||
return result
|
|
||||||
time.sleep(interval)
|
|
||||||
raise Exception(("Timed out after %s seconds. Started on %s and ended "
|
|
||||||
"on %s") % (timeout, start_time, end_time))
|
|
Loading…
Reference in New Issue
Block a user