Create an initial Automation Validation Testing framework

- Setting up a runtime environment
- Add to Zuul 'check' and 'gate' pipeline
- Validate concepts:
  1) List a few unit tests successfully
  2) Execute a few simple tests successfully (pass or fail)
  3) Generate docsbuild successfully
- Each UC will have their own test suite and test cases
and will be handled seperately
- add compute-utility deployment into the gate
- add logging collector of primary
- add false / positive test cases
- add unit-test and feature-test gates

Change-Id: I55a0dcf440e9694b041d5fb8eb75bd6f4adb8913
This commit is contained in:
Trung Thai 2020-04-11 08:25:49 +00:00
parent 942c19243b
commit b0e8e0f478
34 changed files with 1200 additions and 4 deletions

View File

@ -34,6 +34,7 @@ COMMIT ?= $(shell git rev-parse HEAD)
DISTRO_SUFFIX ?= $(DISTRO)
IMAGE = $(DOCKER_REGISTRY)/$(IMAGE_PREFIX)/$(IMAGE_NAME):$(IMAGE_TAG)$(IMAGE_TAG_SUFFIX)
BASE_IMAGE ?=
# TODO(roman_g): DISTRO_SUFFIX should be autogenerated
# from Dockerfile extensions, see $(suffix ) Makefile function
ifeq "$(DISTRO_SUFFIX)" ""
@ -129,8 +130,14 @@ run_images:
run_$(IMAGE_NAME):
@echo "Not implemented." >&2; exit 2
tests:
@echo "Not implemented." >&2; exit 2
unit_tests:
@echo "Run Unit Validation Testing"
./tools/run_avt.sh unit_tests
feature_tests:
@echo "Run Feature Validation Testing"
./tools/run_avt.sh feature_tests
tox -e docs
format:
@echo "Not implemented." >&2; exit 2
@ -176,5 +183,4 @@ endif
.PHONY: $(CHARTS) $(IMAGES) all build chartbanner charts check-docker clean \
docs dry-run-% dry-run format helm-init-% helm-install helm-lint-% \
helm-lint helm-serve images info lint run_$(IMAGE_NAME) run_images \
tests
helm-lint helm-serve images info lint run_$(IMAGE_NAME) run_images

4
docs/requirements.txt Normal file
View File

@ -0,0 +1,4 @@
sphinx>=1.6.2
sphinx_rtd_theme==0.2.4
falcon==1.2.0
oslo.config==6.6.2

137
docs/source/conf.py Normal file
View File

@ -0,0 +1,137 @@
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.todo',
'sphinx.ext.viewcode'
]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.md'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Porthole'
copyright = u'2020, Porthole Authors'
author = u'Porthole Authors'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u'0.1.0'
# The full version, including alpha/beta/rc tags.
release = u'0.1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = []
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
import sphinx_rtd_theme
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# NOTE(mark-burnett): Currently, we don't have any static files and the
# non-existence of this directory causes a sphinx exception.
#html_static_path = ['_static']
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'portholedoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'porthole.tex', u'Porthole Documentation',
u'Porthole Authors', 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'Porthole', u'Porthole Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'Porthole', u'Porthole Documentation',
author, 'Porthole',
'Tool for bootstrapping a resilient Kubernetes cluster and managing its life-cycle.',
'Miscellaneous'),
]

7
docs/source/index.md Normal file
View File

@ -0,0 +1,7 @@
# Utility Containers
Utility containers give Operations staff an interface to an Airship
environment that enables them to perform routine operations and
troubleshooting activities. Utility containers support Airship
environments without exposing secrets and credentials while at
the same time restricting access to the actual containers.

View File

View File

@ -0,0 +1,97 @@
# Copyright 2020 AT&T Intellectual Property. All other rights reserved.
#
# 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 kubeconfig import KubeConfig
class KubeCfg(KubeConfig):
"""This class inherits from the KubeConfig module. It overides the
set_credentials method to add the user exec parameters to the kube config
file that is generated.
"""
def set_credentials(
self,
name,
auth_provider=None,
auth_provider_args=None,
client_certificate=None,
client_key=None,
embed_certs=None,
password=None,
token=None,
username=None,
exec_command=None,
exec_api_version=None,
exec_arg=None,
exec_env=None):
"""Creates or updates a ``user`` entry under the ``users`` entry.
In the case where you are updating an existing user, only the optional
keyword args that you pass in will be updated on the entry.
:param str name: The name of the user to add or update.
:param str auth_provider: The auth provider name to use. For example,
``oidc``, ``gcp``, etc.
:param dict auth_provider_args: Some providers support extra config
params, which can be passed in as a flat dict.
:param str client_certificate: Path to your X.509 client cert (if
using cert auth).
:param str client_key: Path to your cert's private key (if using
cert auth).
:param bool embed_certs: Combined with ``client_certificate``,
setting this to ``True`` will cause the cert to be embedded
directly in the written config. If ``False`` or unspecified,
the path to the cert will be used instead.
:param str username: Your username (if using basic auth).
:param str password: Your user's password (if using basic auth).
:param str token: Your private token (if using token auth).
:param str exec_command: The command executable name to use. For
example, ``client-keystone-auth``
:param str exec_api_version: The api version to use. For example,
``client.authentication.k8s.io/v1beta1``
"""
flags = []
if auth_provider is not None:
flags += ['--auth-provider=%s' % auth_provider]
if auth_provider_args is not None:
arg_pairs = [
"%s=%s" % (k, v) for k, v in auth_provider_args.items()
]
for arg_pair in arg_pairs:
flags += ['--auth-provider-arg=%s' % arg_pair]
if client_certificate is not None:
flags += ['--client-certificate=%s' % client_certificate]
if client_key is not None:
flags += ['--client-key=%s' % client_key]
if embed_certs is not None:
flags += ['--embed-certs=%s' % self._bool_to_cli_str(embed_certs)]
if password is not None:
flags += ['--password=%s' % password]
if token is not None:
flags += ['--token=%s' % token]
if username is not None:
flags += ['--username=%s' % username]
if exec_command is not None:
flags += ['--exec-command=%s' % exec_command]
if exec_api_version is not None:
flags += ['--exec-api-version=%s' % exec_api_version]
if exec_arg is not None:
flags += ['--exec-arg=%s' % exec_arg]
if exec_env is not None:
arg_pairs = ["%s=%s" % (k, v) for k, v in exec_env.items()]
for arg_pair in arg_pairs:
flags += ['--exec-env=%s' % arg_pair]
self._run_kubectl_config('set-credentials', name, *flags)

View File

@ -0,0 +1,63 @@
# Copyright 2020 AT&T Intellectual Property. All other rights reserved.
#
# 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.
class KubeUtilityContainerException(Exception):
"""Class for Kube Utility Container Plugin Exceptions"""
def __init__(self, error="", message=""):
self.error = error or self.__class__.error
self.message = message or self.__class__.message
super(KubeUtilityContainerException, self).__init__(
''.join([self.error, '::', self.message]))
class KubeConfigException(Exception):
"""Exception class when Kubernetes config is not found"""
def __init__(self, message):
self.message = "Kubernetes config not found: {}".format(message)
super(KubeConfigException, self).__init__(self.message)
class KubeApiException(Exception):
"""Exception class for error in accessing Kubernetes APIs"""
def __init__(self, message):
self.message = "Exception occurred while accessing Kubernetes APIs: " \
"{}".format(message)
super(KubeApiException, self).__init__(self.message)
class KubeDeploymentNotFoundException(Exception):
"""Exception class for Kube Deployment not found in a namespace"""
def __init__(self, message):
self.message = "Deployment not found Error: {}".format(message)
super(KubeDeploymentNotFoundException, self).__init__(self.message)
class KubePodNotFoundException(Exception):
"""Exception class for specific utility pod not found in running state"""
def __init__(self, message):
self.message = "Pod not found: {}".format(message)
super(KubePodNotFoundException, self).__init__(self.message)
class KubeEnvVarException(Exception):
"""Exception class for environment variable not set."""
def __init__(self, message):
self.message = "Environment Variable Not Found: {}".format(message)
super(KubeEnvVarException, self).__init__(self.message)

View File

@ -0,0 +1,247 @@
# Copyright 2020 AT&T Intellectual Property. All other rights reserved.
#
# 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 pathlib import Path
from kube_utility_container.kubecfg.kube_cfg import KubeCfg
from kube_utility_container.services.exceptions import \
KubeApiException
from kube_utility_container.services.exceptions import \
KubeConfigException
from kube_utility_container.services.exceptions import \
KubeDeploymentNotFoundException
from kube_utility_container.services.exceptions import \
KubeEnvVarException
from kube_utility_container.services.exceptions import \
KubePodNotFoundException
from kubernetes import client as kubeclient
from kubernetes import config as kubeconf
from kubernetes.client.rest import ApiException
from kubernetes.stream import stream
from oslo_log import log as logging
from urllib3.exceptions import MaxRetryError
LOG = logging.getLogger(__name__)
class UtilityContainerClient(object):
"""Client to execute utilscli command on utility containers"""
NAMESPACE = 'utility'
def __init__(self):
# Initialize variables
self._corev1api_client = None
self._appsv1api_client = None
@property
def _corev1api_api_client(self):
"""Property to get the V1CoreAPI client object"""
if self._corev1api_client:
return self._corev1api_client
else:
try:
kubeconf.load_kube_config(config_file=self._kubeconfig_file)
self._corev1api_client = kubeclient.CoreV1Api()
return self._corev1api_client
except EnvironmentError as err:
LOG.exception(
'Failed to load Kubernetes config file: {}'.format(err))
raise KubeConfigException(err)
@property
def _appsv1api_api_client(self):
"""Property to get the V1AppsAPI client object"""
if self._appsv1api_client:
return self._appsv1api_client
else:
try:
kubeconf.load_kube_config(config_file=self._kubeconfig_file)
self._appsv1api_client = kubeclient.AppsV1Api()
return self._appsv1api_client
except EnvironmentError as err:
LOG.exception(
'Failed to load Kubernetes config file: {}'.format(err))
raise KubeConfigException(err)
@property
def _kubeconfig_file(self):
"""Property to generate kubeconfig file from environment variables"""
key = 'KUBECONFIG'
if os.environ.get(key) is not None:
kube_conf_filename = os.environ.get(key)
else:
raise KubeEnvVarException(key)
if os.path.isfile(kube_conf_filename):
return kube_conf_filename
else:
self._prepare_kube_config(kube_conf_filename)
return kube_conf_filename
def _prepare_kube_config(self, kube_conf_filename):
"""Method to generate the kube config file"""
Path(Path.cwd() / 'etc').mkdir(exist_ok=True)
Path(kube_conf_filename).touch()
conf = KubeCfg(kube_conf_filename)
region_key = 'OS_REGION_NAME'
kube_server_key = 'KUBE_SERVER'
if os.environ.get(region_key) is not None:
server = os.environ.get(kube_server_key)
else:
raise KubeEnvVarException(kube_server_key)
if os.environ.get(region_key) is not None:
conf.set_cluster(
name=os.environ.get(region_key),
server=server,
insecure_skip_tls_verify=True)
else:
raise KubeEnvVarException(region_key)
username_key = 'OS_USERNAME'
if os.environ.get(username_key) is not None:
conf.set_context(
name='context_uc',
user=os.environ.get(username_key),
namespace='utility',
cluster=os.environ.get(region_key))
else:
raise KubeEnvVarException(username_key)
conf.use_context('context_uc')
exec_command_key = 'KUBE_KEYSTONE_AUTH_EXEC'
if os.environ.get(exec_command_key) is not None:
conf.set_credentials(
name=os.environ.get(username_key),
exec_command=os.environ.get(exec_command_key),
exec_api_version='client.authentication.k8s.io/v1beta1')
else:
raise KubeEnvVarException(exec_command_key)
def _get_deployment_selectors(self, deployment_name):
"""Method to get the deployment selectors of the deployment queried.
:param deployment_name: if specified the deployment name of the utility pod
where the utilscli command is to be executed.
:type deployment_name: string
where the utilscli command is to be executed.
:return: selectors extracted from the deployment
returned as a string in the format: "key=value, key1=value2..."
:exception:
KubeDeploymentNotFoundException -- A custom exception
KubeDeploymentNotFoundException is raised if no deployment is
found with the with the parameters namespace and deployment_name
which is passed as a field_selector.
"""
# Get a specific deployment by passing the deployment metadata name
# and the namespace
deployment = self._appsv1api_api_client.list_namespaced_deployment(
self.NAMESPACE,
field_selector='metadata.name={}'.format(deployment_name)).items
if deployment:
# Get the selectors from the deployment object returned.
selector_dict = deployment[0].spec.selector.match_labels
# Convert the selector dictionary to a string object.
selectors = ', '.join(
"{!s}={!s}".format(k, v) for (k, v) in selector_dict.items())
return selectors
else:
raise KubeDeploymentNotFoundException(
'Deployment with name {} not found in {} namespace'.format(
deployment_name, self.NAMESPACE))
def _get_utility_container(self, deployment_name):
"""Method to get a specific utility container filtered by the selectors
:param deployment_name: if specified the deployment name of the utility pod
where the utilscli command is to be executed.
:type deployment_name: string
where the utilscli command is to be executed.
:return: selectors extracted from the deployment
utility_container {V1Pod} -- Returns the first pod matched.
:exception: KubePodNotFoundException -- Exception raised if not pods are found.
"""
deployment_selectors = self._get_deployment_selectors(deployment_name)
utility_containers = self._corev1api_api_client.list_namespaced_pod(
self.NAMESPACE, label_selector=deployment_selectors).items
if utility_containers:
return utility_containers[0]
else:
raise KubePodNotFoundException(
'No Pods found in Deployment {} with selectors {} in {} '
'namespace'.format(
deployment_name, deployment_selectors, self.NAMESPACE))
def _get_exec_cmd_output(self, utility_container, ex_cmd, default=1):
"""Exec into a specific utility container, then execute the utilscli
command and return the output of the command
:params utility_container: Utility container where the
utilscli command will be executed.
:type utility_container: string
:params ex_cmd: command to be executed inside the utility container
:type ex_cmd: strings
:params default: return of cmd_output. optionally can be disabled
:type integer: 1 (true)
:type ex_cmd: strings
:return: Output of command executed in the utility container
"""
try:
container = utility_container.spec.containers[0].name
LOG.info(
'\nPod Name: {} \nNamespace: {} \nContainer Name: {} '
'\nCommand: {}'.format(
utility_container.metadata.name, self.NAMESPACE, container,
ex_cmd))
cmd_output = stream(
self._corev1api_api_client.connect_get_namespaced_pod_exec,
utility_container.metadata.name,
self.NAMESPACE,
container=container,
command=ex_cmd,
stderr=True,
stdin=False,
stdout=True,
tty=False)
LOG.info(
'Pod Name: {} Command Output: {}'.format(
utility_container.metadata.name, cmd_output))
if default is 1:
return cmd_output
except (ApiException, MaxRetryError) as err:
LOG.exception(
"An exception occurred in pod "
"exec command: {}".format(err))
raise KubeApiException(err)
def exec_cmd(self, deployment_name, cmd):
"""Get specific utility container using deployment name, call
method to execute utilscli command and return the output of
the command.
:params deployment_name: deployment name of the utility pod
where the utilscli command is to be executed.
:type deployment_name: string
:params cmd: command to be executed inside the utility container
:type cmd: strings
:return: Output of command executed in the utility container
"""
utility_container = self._get_utility_container(deployment_name)
return self._get_exec_cmd_output(utility_container, cmd)

View File

View File

@ -0,0 +1,103 @@
# Copyright 2020 AT&T Intellectual Property. All other rights reserved.
#
# 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 kubernetes import client
import unittest
from unittest.mock import MagicMock as Mock
from unittest.mock import patch
from kube_utility_container.services.exceptions import \
KubeDeploymentNotFoundException
from kube_utility_container.services.exceptions import \
KubeEnvVarException
from kube_utility_container.services.exceptions import \
KubePodNotFoundException
from kube_utility_container.services.utility_container_client import \
UtilityContainerClient
class TestUtilityContainerClient(unittest.TestCase):
"""Unit tests for Utility Container Client"""
@patch(
'kube_utility_container.services.utility_container_client.'
'UtilityContainerClient._get_utility_container')
@patch(
'kube_utility_container.services.utility_container_client.'
'UtilityContainerClient._get_exec_cmd_output')
def test_exec_cmd(self, mock_get_exec_cmd_output, mock_utility_container):
v1_container_obj = Mock(
spec=client.V1Container(
name='ceph_utility', image='sha', image_pull_policy='Always'))
v1_spec_obj = Mock(spec=client.V1PodSpec(containers=v1_container_obj))
v1_meta_obj = Mock(
spec=client.V1ObjectMeta(
name='clcp-ceph-utility-5454794df8-xqwj5', labels='app=ceph'))
v1_pod_obj = Mock(
spec=client.V1Pod(
api_version='v1', metadata=v1_meta_obj, spec=v1_spec_obj))
mock_utility_container.return_value = v1_pod_obj
mock_get_exec_cmd_output.return_value = "Health OK"
utility_container_client = UtilityContainerClient()
response = utility_container_client.exec_cmd(
'clcp-utility', ['utilscli', 'ceph', 'status'])
self.assertIsNotNone(response)
self.assertIsInstance(response, str)
self.assertEqual(response, mock_get_exec_cmd_output.return_value)
@patch(
'kube_utility_container.services.utility_container_client.'
'UtilityContainerClient._get_utility_container',
side_effect=KubePodNotFoundException('utility'))
def test_exec_cmd_no_utility_pods_returned(self, mock_list_pods):
mock_list_pods.return_value = []
utility_container_client = UtilityContainerClient()
with self.assertRaises(KubePodNotFoundException):
utility_container_client.exec_cmd(
'clcp-utility', ['utilscli', 'ceph', 'status'])
@patch(
'kube_utility_container.services.utility_container_client.'
'UtilityContainerClient._get_deployment_selectors',
side_effect=KubeDeploymentNotFoundException('utility'))
@patch(
'kube_utility_container.services.utility_container_client.'
'UtilityContainerClient._corev1api_api_client')
def test_exec_cmd_no_deployments_returned(self, deployment, api_client):
deployment.return_value = []
api_client.return_value = []
utility_container_client = UtilityContainerClient()
with self.assertRaises(KubeDeploymentNotFoundException):
utility_container_client.exec_cmd(
'clcp-ceph-utility', ['utilscli', 'ceph', 'status'])
@patch(
'kube_utility_container.services.utility_container_client.'
'UtilityContainerClient._get_deployment_selectors',
side_effect=KubeEnvVarException('utility'))
@patch(
'kube_utility_container.services.utility_container_client.'
'UtilityContainerClient._appsv1api_api_client',
side_effect=KubeEnvVarException('KUBECONFIG'))
def test_env_var_kubeconfig_not_set_raises_exception(self, deployment, api_client):
deployment.return_value = []
api_client.return_value = []
utility_container_client = UtilityContainerClient()
with self.assertRaises(KubeEnvVarException):
utility_container_client.exec_cmd(
'clcp-ceph-utility', ['utilscli', 'ceph', 'status'])

View File

@ -0,0 +1,25 @@
# Copyright 2020 AT&T Intellectual Property. All other rights reserved.
#
# 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 unittest
from kube_utility_container.services.utility_container_client\
import UtilityContainerClient
class TestBase(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.client = UtilityContainerClient()

View File

@ -0,0 +1,32 @@
# Copyright 2020 AT&T Intellectual Property. All other rights reserved.
#
# 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 unittest
from kube_utility_container.tests.utility.base import TestBase
class TestCalicoUtilityContainer(TestBase):
@classmethod
def setUpClass(cls):
cls.deployment_name = 'calicoctl-utility'
super(TestCalicoUtilityContainer, cls).setUpClass()
def test_verify_calico_client_calicoctl_is_present(self):
"""To verify calico-utility calicoctl is present"""
exec_cmd = ['utilscli', 'calicoctl', 'version']
expected = 'Client Version:'
result_set = self.client.exec_cmd(self.deployment_name, exec_cmd)
self.assertIn(
expected, result_set, 'Unexpected value for command: {}, '
'Command Output: {}'.format(exec_cmd, result_set))

View File

@ -0,0 +1,31 @@
# Copyright 2020 AT&T Intellectual Property. All other rights reserved.
#
# 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 unittest
from kube_utility_container.tests.utility.base import TestBase
class TestCephUtilityContainer(TestBase):
@classmethod
def setUpClass(cls):
cls.deployment_name = 'ceph-utility'
super(TestCephUtilityContainer, cls).setUpClass()
def test_verify_ceph_is_healthy(self):
"""To verify ceph-utility is healthy"""
exec_cmd = ['utilscli', 'ceph', 'status']
expected = 'HEALTH_OK'
result_set = self.client.exec_cmd(self.deployment_name, exec_cmd)
self.assertIn(
expected, result_set, 'Unexpected value for command: {}, '
'Command Output: {}'.format(exec_cmd, result_set))

View File

@ -0,0 +1,49 @@
# Copyright 2020 AT&T Intellectual Property. All other rights reserved.
#
# 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 unittest
import re
import os
from kube_utility_container.tests.utility.base import TestBase
node = os.uname().nodename
class TestComputeUtilityContainer(TestBase):
@classmethod
def setUpClass(cls):
cls.deployment_name = 'compute-utility'
super(TestComputeUtilityContainer, cls).setUpClass()
@unittest.expectedFailure
def test_verify_compute_ovsclient_is_present(self):
"""To verify compute-utility ovs-client is present."""
cmd = 'ovs-client '
exec_cmd = ['utilscli', cmd + node, 'ovs-vsctl -V']
expected = 'ovs-vsctl'
result_set = self.client.exec_cmd(self.deployment_name, exec_cmd)
self.assertIn(
expected, result_set, 'Unexpected value for command: {}, '
'Command Output: {}'.format(exec_cmd, result_set))
@unittest.expectedFailure
def test_verify_compute_libvirtclient_is_present_on_host(self):
"""To verify compute-utility Libvirt-client is present."""
cmd = 'libvirt-client '
exec_cmd = ['utilscli', cmd + node, 'virsh list']
expected = 'Id'
result_set = self.client.exec_cmd(self.deployment_name, exec_cmd)
self.assertIn(
expected, result_set, 'Unexpected value for command: {}, '
'Command Output: {}'.format(exec_cmd, result_set))

View File

@ -0,0 +1,43 @@
# Copyright 2020 AT&T Intellectual Property. All other rights reserved.
#
# 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 re
import unittest
from kube_utility_container.tests.utility.base import TestBase
class TestEtcdUtilityContainer(TestBase):
@classmethod
def setUpClass(cls):
cls.deployment_name = 'etcdctl-utility'
super(TestEtcdUtilityContainer, cls).setUpClass()
def test_verify_etcd_ctl_is_present(self):
"""To verify etcdctl-utility etcdctl is present."""
exec_cmd = ['utilscli', 'etcdctl', 'version']
expected = 'etcdctl version:'
result_set = self.client.exec_cmd(self.deployment_name, exec_cmd)
self.assertIn(
expected, result_set, 'Unexpected value for command: {}, '
'Command Output: {}'.format(exec_cmd, result_set))
@unittest.expectedFailure
def test_verify_etcd_endpoint_is_healthy(self):
"""To verify etcdctl-utility endpoint is healthy"""
exec_cmd = ['utilscli', 'etcdctl', 'endpoint health']
expected = 'is health: successfully'
result_set = self.client.exec_cmd(self.deployment_name, exec_cmd)
self.assertIn(
expected, result_set, 'Unexpected value for command: {}, '
'Command Output: {}'.format(exec_cmd, result_set))

56
requirements-frozen.txt Normal file
View File

@ -0,0 +1,56 @@
Babel==2.8.0
cachetools==4.0.0
certifi==2020.4.5.1
chardet==3.0.4
cliff==3.1.0
cmd2==0.8.9
coverage==4.5.1
debtcollector==2.0.1
extras==1.0.0
fixtures==3.0.0
future==0.18.2
google-auth==1.13.1
idna==2.9
iso8601==0.1.12
kubeconfig==1.0.1
kubernetes==10.0.1
linecache2==1.0.0
monotonic==1.5
msgpack==1.0.0
netaddr==0.7.19
netifaces==0.10.9
oauthlib==3.1.0
oslo.config==6.7.0
oslo.context==3.0.2
oslo.i18n==4.0.1
oslo.log==3.40.1
oslo.serialization==3.1.1
oslo.utils==4.1.1
pbr==3.1.1
prettytable==0.7.2
pyasn1==0.4.8
pyasn1-modules==0.2.8
pyinotify==0.9.6
pyparsing==2.4.7
pyperclip==1.8.0
pytest==5.4.1
python-dateutil==2.8.1
python-mimeparse==1.6.0
python-subunit==1.4.0
pytz==2019.3
PyYAML==5.3.1
requests==2.23.0
requests-oauthlib==1.3.0
rfc3986==1.4.0
rsa==4.0
six==1.14.0
stestr==2.1.1
stevedore==1.32.0
testtools==2.4.0
traceback2==1.4.0
unittest2==1.1.0
urllib3==1.25.8
voluptuous==0.11.7
wcwidth==0.1.9
websocket-client==0.57.0
wrapt==1.12.1

12
requirements.txt Normal file
View File

@ -0,0 +1,12 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
# When modifying this file `tox -e freeze-req` must be run to regenerate the requirements-frozen.txt.
coverage==4.5.1
kubeconfig==1.0.1
kubernetes==10.0.1
oslo.config==6.7.0 # Apache-2.0
oslo.log==3.40.1 # Apache-2.0
pbr==3.1.1
stestr==2.1.1 # Apache-2.0

21
setup.cfg Normal file
View File

@ -0,0 +1,21 @@
[metadata]
name = porthole_plugin
version = 1.0.0
summary = 'This plugin consists set of testcases for utility containers based on kubernetes exec api.'
description-file = README.rst
author = The Airship Authors
author-email = airship-discuss@lists.airshipit.org
home-page = https://opendev.org/airship/porthole
classifier =
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
[files]
packages =
porthole

27
setup.py Normal file
View File

@ -0,0 +1,27 @@
#!/usr/bin/env python3
# Copyright 2020 AT&T Intellectual Property. All other rights reserved.
#
# 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 setuptools import setup
try:
import multiprocessing # noqa
except ImportError:
pass
setup(
setup_requires=['setuptools>=17.1', 'pbr>=2.0.0'],
pbr=True
)

22
test-requirements.txt Normal file
View File

@ -0,0 +1,22 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
# When modifying this file `tox -e freeze-testreq` must be run to regenerate the test-requirements-frozen.txt.
astroid==2.3.3
bandit==1.5.1
flake8==3.6.0
hacking==0.12.0 # Apache-2.0
coverage==4.5.1 # Apache-2.0
pylint==2.4.4
python-subunit==1.3.0 # Apache-2.0/BSD
oslotest==3.7.0 # Apache-2.0
stestr==2.1.1 # Apache-2.0
testtools==2.3.0 # MIT
mock==3.0.5
nose==1.3.7
responses==0.10.2
yapf==0.24.0

View File

@ -0,0 +1,23 @@
# 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.
- hosts: primary
tasks:
- name: Execute the make target for features testing
make:
chdir: "{{ zuul.project.src_dir }}"
target: feature_tests
register: result
failed_when: result.failed
- name: Include collecting running logs
import_playbook: airship-porthole-collect-logs.yaml

View File

@ -0,0 +1,23 @@
# 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.
- hosts: primary
tasks:
- name: Execute the make target for unit testing
make:
chdir: "{{ zuul.project.src_dir }}"
target: unit_tests
register: result
failed_when: result.failed
- name: Include collecting running logs
import_playbook: airship-porthole-collect-logs.yaml

58
tools/run_avt.sh Executable file
View File

@ -0,0 +1,58 @@
#!/bin/bash
# Copyright 2020 AT&T Intellectual Property. All other rights reserved.
#
# 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.
set -x
TYPE=$1
VENV=$(mktemp -d)
PLUGINS=kube_utility_container
export KUBECONFIG=${KUBECONFIG:-~/.kube/config}
function setup_venv() {
sudo apt-get install libffi-dev libssl-dev -y
python3 -m venv ${VENV}
if [[ -f ${VENV}/bin/activate ]] ;then
source $VENV/bin/activate
${VENV}/bin/pip install -r requirements-frozen.txt
${VENV}/bin/python -m pip list --format=columns
kubectl get deployment -n utility
kubectl get nodes -o wide
kubectl get po --all-namespaces -o wide
stestr init
fi
}
function run_avt() {
setup_venv
if [[ ${TYPE} == 'unit_tests' ]] ; then
run_unit_tests
elif [[ ${TYPE} == 'feature_tests' ]] ; then
run_feature_tests
else
echo "No validating tests performed..skip"
fi
}
function run_feature_tests() {
python -m unittest discover -s ${PLUGINS}/tests/utility/compute -v
python -m unittest discover -s ${PLUGINS}/tests/utility/etcd -v
python -m unittest discover -s ${PLUGINS}/tests/utility/calico -v
python -m unittest discover -s ${PLUGINS}/tests/utility/ceph -v
}
function run_unit_tests() {
python -m unittest discover -s ${PLUGINS}/tests/unit/services -v
}
run_avt

56
tox.ini Normal file
View File

@ -0,0 +1,56 @@
[tox]
minversion = 3.4
envlist = dev,pep8,py36,bandit,docs,list-tests
skipsdist = true
[testenv:dev]
useddevelop = True
basepython = python3
passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY
setenv =
VIRTUAL_ENV={envdir}
PYTHONWARNINGS=default::DeprecationWarning
deps =
freeze-req: -r{toxinidir}/requirements-frozen.txt
list-tests: -r {toxinidir}/requirements-frozen.txt
envdir=
{toxinidir}/.tox/{envname}
commands_pre =
find . -type f -name "*.pyc"
list-tests: stestr init
[testenv:venv]
commands = {posargs}
[testenv:py36]
setenv =
PYTHONWARNING=all
deps = -r{toxinidir}/requirements-frozen.txt
-r{toxinidir}/test-requirements.txt
commands =
pytest {posargs}
[testenv:bandit]
deps =
-r{toxinidir}/test-requirements.txt
commands =
bandit -r {toxinidir}
[testenv:docs]
whitelist_externals = rm
deps =
-r{toxinidir}/docs/requirements.txt
commands =
rm -rf docs/build
sphinx-build -W -b html docs/source docs/build/html
[testenv:pep8]
deps =
-r{toxinidir}/test-requirements.txt
commands =
yapf -rd {toxinidir} {toxinidir}/tests
flake8 {toxinidir}
bandit -r {toxinidir}

View File

@ -25,6 +25,8 @@
- airship-porthole-images-build-gate-openstack-utility
- airship-porthole-images-build-gate-postgresql-utility
- airship-porthole-deploy
- airship-porthole-unit-tests
- airship-porthole-feature-tests
gate:
jobs:
@ -39,6 +41,8 @@
- airship-porthole-deploy
- airship-porthole-apparmor:
voting: false
- airship-porthole-unit-tests
- airship-porthole-feature-tests
experimental:
jobs:
@ -130,6 +134,56 @@
args:
chdir: "{{ zuul.project.src_dir }}"
- job:
name: airship-porthole-unit-tests
description: |
Executes unit tests
dependencies:
- airship-porthole-deploy
run: tools/gate/playbooks/airship-porthole-gate-runner.yaml
nodeset: airship-porthole-single-node
timeout: 7200
post-run: tools/gate/playbooks/make-unit-tests.yaml
vars:
gate_scripts:
- ./tools/deployment/utilities/000-install-packages.sh
- ./tools/deployment/utilities/001-setup-apparmor-profiles.sh
- ./tools/deployment/utilities/002-deploy-k8s.sh
- ./tools/deployment/utilities/005-calicoctl-utility.sh
- ./tools/deployment/utilities/010-ceph-utility.sh
- ./tools/deployment/utilities/020-compute-utility.sh
- ./tools/deployment/utilities/030-etcdctl-utility.sh
- ./tools/deployment/utilities/040-mysqlclient-utility.sh
- ./tools/deployment/utilities/050-openstack-utility.sh
- ./tools/deployment/utilities/060-postgresql-utility.sh
args:
chdir: "{{ zuul.project.src_dir }}"
- job:
name: airship-porthole-feature-tests
description: |
Executes feature tests
dependencies:
- airship-porthole-deploy
run: tools/gate/playbooks/airship-porthole-gate-runner.yaml
nodeset: airship-porthole-single-node
timeout: 7200
post-run: tools/gate/playbooks/make-feature-tests.yaml
vars:
gate_scripts:
- ./tools/deployment/utilities/000-install-packages.sh
- ./tools/deployment/utilities/001-setup-apparmor-profiles.sh
- ./tools/deployment/utilities/002-deploy-k8s.sh
- ./tools/deployment/utilities/005-calicoctl-utility.sh
- ./tools/deployment/utilities/010-ceph-utility.sh
- ./tools/deployment/utilities/020-compute-utility.sh
- ./tools/deployment/utilities/030-etcdctl-utility.sh
- ./tools/deployment/utilities/040-mysqlclient-utility.sh
- ./tools/deployment/utilities/050-openstack-utility.sh
- ./tools/deployment/utilities/060-postgresql-utility.sh
args:
chdir: "{{ zuul.project.src_dir }}"
- secret:
name: quay_credentials
data: