Add cloudtaf framework
[ta/cloudtaf.git] / libraries / openstackcli / openstackcli.py
diff --git a/libraries/openstackcli/openstackcli.py b/libraries/openstackcli/openstackcli.py
new file mode 100644 (file)
index 0000000..72559ca
--- /dev/null
@@ -0,0 +1,404 @@
+# Copyright 2019 Nokia
+#
+# 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.
+
+"""Test library for running *openstack* in remote system.
+"""
+import abc
+import json
+from collections import namedtuple
+from contextlib import contextmanager
+import six
+
+
+class OpenStackCliError(Exception):
+    """Exception raised in case CLI in the remote system fails.
+    """
+    pass
+
+
+class _FailedMessage(namedtuple('FailedMessage', ['cmd', 'target'])):
+    def __str__(self):
+        return "Remote command '{cmd}' in target '{target}' failed".format(
+            cmd=self.cmd,
+            target=self.target)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class _TargetBase(object):
+
+    @abc.abstractproperty
+    def target(self):
+        """Return *RemoteSession* target where command is to be executed
+        """
+
+    @abc.abstractproperty
+    def cmd_template(self):
+        """Return cmd template with field *cli*, *cmd* and *fmt*.
+        """
+
+
+class _DefaultTarget(_TargetBase):
+    def __init__(self):
+        self._raw_target = None
+        self._target = None
+        self._cloud = 'default'
+
+    def set_target(self, target):
+        self._raw_target = target
+        self._setup()
+
+    def _setup(self):
+        self._target = self._raw_target
+        if self._partslen == 2 or self._partslen == 3:
+            self._update_target_and_cloud()
+
+    def _update_target_and_cloud(self):
+        if self._oper == 'os-cloud':
+            self._target = self._parts[0] if self._partslen == 3 else 'default'
+            self._cloud = self._parts[-1]
+        elif self._partslen == 2:
+            self._cloud = None
+
+    @property
+    def _oper(self):
+        return self._parts[self._partslen - 2]
+
+    @property
+    def _parts(self):
+        return self._raw_target.split('.')
+
+    @property
+    def _partslen(self):
+        return len(self._parts)
+
+    @property
+    def target(self):
+        return self._target
+
+    @property
+    def cmd_template(self):
+        return ('{cli} {cmd}{fmt}'
+                if self._cloud is None else
+                '{{cli}} --os-cloud {cloud} {{cmd}}{{fmt}}'.format(cloud=self._cloud))
+
+
+class _CliRunnerInTarget(object):
+    def __init__(self, run, target):
+        self._run = run
+        self._target = target
+        self._cli = None
+
+    def set_cli(self, cli):
+        self._cli = cli
+
+    def run_with_json(self, cmd):
+        return self._run_with_verification(cmd, fmt=' -f json')
+
+    def run(self, cmd):
+        return self._run_with_verification(cmd)
+
+    def _run_with_verification(self, cmd, fmt=''):
+        cmd = self._get_formatted_cmd(cmd, fmt)
+        result = self._run(cmd, self._target.target)
+        self._verify_result(result, _FailedMessage(cmd, self._target.target))
+        return result
+
+    def run_without_verification(self, cmd, fmt=''):
+        return self._run(self._get_formatted_cmd(cmd, fmt), self._target.target)
+
+    def _get_formatted_cmd(self, cmd, fmt):
+        return self._target.cmd_template.format(cli=self._cli,
+                                                cmd=cmd,
+                                                fmt=fmt)
+
+    def _verify_result(self, result, failedmessage):
+        status = self._get_integer_status(result, failedmessage)
+        if status or result.stderr:
+            self._raise_openstackerror(failedmessage, result)
+        return result
+
+    def _get_integer_status(self, result, failedmessage):
+        try:
+            return int(result.status)
+        except ValueError:
+            self._raise_openstackerror(failedmessage, result)
+
+    @staticmethod
+    def _raise_openstackerror(failedmessage, result):
+        raise OpenStackCliError(
+            '{failedmessage}: status: {status!r}, '
+            'stdout: {stdout!r}, '
+            'stderr: {stderr!r}'.format(failedmessage=failedmessage,
+                                        status=result.status,
+                                        stdout=result.stdout,
+                                        stderr=result.stderr))
+
+
+class OpenStack(object):
+    """Remote *openstack* runner.
+    The runner executes commands in *crl.remotesession* target but the target
+    *os-cloud.cloudconfigname* is interpreted to *--os-cloud cloudconfigname*
+    argument and executed in the *default* target. Respectively, the target
+    *controller-1.os-cloud.cloudconfigname* is executed in *controller-1*
+    target with *--os-cloud cloudconfigname*.
+
+    If the *target* is of form *target.envname*, then *--os-cloud* is not given
+    as *crl.remotesession* environment handling should then take care of
+    setting the correct environment for the *envname* in *target*.
+    """
+
+    def __init__(self):
+        self._remotesession = None
+        self._envname = None
+        self._openrc_tuples = {}
+
+    def initialize(self, remotesession, envname=None):
+        """Initialize the library.
+
+        Args:
+            remotesession: `crl.remotesession.remotesession.RemoteSession`_
+                           instance
+
+            envname: Environment name appended to the target in form target.envname.
+                     See Robot example 2 for details of the usage.
+
+        **Note:**
+
+           Targets are interpreted in the following manner:
+
+           ============================== ==================== ====================
+           Target                         RemoteSession target os-cloud config name
+           ============================== ==================== ====================
+           os-cloud.confname              default              confname
+           controller-1.os-cloud.confname controller-1         confname
+           target.envname                 target.envname       None
+           ============================== ==================== ====================
+
+        **Robot Examples**
+
+            In the following example is assumed that
+            `crl.remotesession.remotesession.RemoteSession`_ is imported in
+            Library settings *WITH NAME* RemoteSession.
+
+
+        *Example 1*
+
+            ====================   =====================  =============
+            ${remotesession}=      Get Library Instance   RemoteSession
+            OpenStack.Initialize   ${remotesession}
+            ====================   =====================  =============
+
+        *Example 2*
+
+            If in the test setup initialization is done with given *myenv* environment
+
+            ====================   =====================  =============
+            ${remotesession}=      Get Library Instance   RemoteSession
+            OpenStack.Initialize   ${remotesession}       envname=myenv
+            ====================   =====================  =============
+
+            Then
+
+            =============  ==========  =============
+            OpenStack.Run  quota show  target=target
+            =============  ==========  =============
+
+            runs *openstack quota show* in the target *target.myenv*.
+
+        .. _crl.remotesession.remotesession.RemoteSession:
+           https://crl-remotesession.readthedocs.io/en/latest
+           /crl.remotesession.remotesession.RemoteSession.html
+        """
+        self._remotesession = remotesession
+        self._envname = envname
+
+    def run(self, cmd, target='default'):
+        """ Run *openstack* in the remote target with *json* format.
+
+        Args:
+            cmd: openstack command to be executed in target.
+
+            target: `crl.remotesession.remotesession.RemoteSession`_ target.
+
+        Returns:
+            Decoded *json* formatted command output. For example in case
+            openstack output (
+            for command *openstack.run('quota show')* is in remote::
+
+                # openstack --os-cloud default quota show -f json
+                {
+                 "secgroups": 10,
+                 "health_monitors": null,
+                 "l7_policies": null,
+                 ...
+                }
+
+            then the return value of *run* is:
+
+            .. code-block:: python
+
+                {
+                  "secgroups": 10,
+                  "health_monitors": None,
+                  "17_policies": None,
+                  ...
+                }
+
+        **Robot Example:**
+
+            ================ ===================== ===========
+            ${quota}=        OpenStack.Run         quota show
+            Should Be Equal  ${quota['secgroups']} 10
+            ================ ===================== ===========
+
+        Raises:
+            OpenStackCliError: if remote *openstack* fails.
+
+        .. _crl.remotesession.remotesession.RemoteSession:
+           https://crl-remotesession.readthedocs.io/en/latest
+           /crl.remotesession.remotesession.RemoteSession.html
+        """
+        result = self._create_runner(target).run_with_json(cmd)
+        with self._error_handling(cmd, target):
+            return json.loads(result.stdout)
+
+    def run_ignore_output(self, cmd, target='default'):
+        """ Run *openstack* in the remote target and ignore the output.
+
+        Args:
+            cmd: openstack command to be executed in target.
+
+            target: `crl.remotesession.remotesession.RemoteSession`_ target.
+
+        Returns:
+            Nothing
+
+        Raises:
+            OpenStackCliError: if remote *openstack* fails.
+
+        .. _crl.remotesession.remotesession.RemoteSession:
+           https://crl-remotesession.readthedocs.io/en/latest
+           /crl.remotesession.remotesession.RemoteSession.html
+        """
+        self._create_runner(target).run(cmd)
+
+    def run_raw(self, cmd, target='default'):
+        """ Run *openstack* in the remote target and return raw output without
+        formatting.
+
+        Args:
+            cmd: openstack command to be executed in target.
+
+            target: `crl.remotesession.remotesession.RemoteSession`_ target.
+
+        Returns:
+            Nothing
+
+        Raises:
+            OpenStackCliError: if remote *openstack* fails.
+
+        .. _crl.remotesession.remotesession.RemoteSession:
+           https://crl-remotesession.readthedocs.io/en/latest
+           /crl.remotesession.remotesession.RemoteSession.html
+        """
+        return self._create_runner(target).run(cmd).stdout
+
+    def run_nohup(self, cmd, target='default'):
+        """ Run *openstack* in the remote target nohup mode in background and
+        return PID of the started process.
+
+        Note:
+            This keyword is available only for targets initialized by
+            *Set Runner Target* keyword of *RemoteSession*. The version of
+            crl.interactivesessions must be at least as new as
+            crl.interactivesessions==1.0b4.
+
+        Args:
+            cmd: openstack command to be executed in target.
+
+            target: `crl.remotesession.remotesession.RemoteSession`_ target.
+
+        Returns:
+            PID of the process running *cmd* in the target.
+
+        Raises:
+            OpenStackCliError: if remote *openstack* fails.
+
+        .. _crl.remotesession.remotesession.RemoteSession:
+           https://crl-remotesession.readthedocs.io/en/latest
+           /crl.remotesession.remotesession.RemoteSession.html
+        """
+        return self._create_runner(
+            target, run=self._nohup_run).run_without_verification(cmd)
+
+    def _create_runner(self, target, run=None):
+        run = self._run if run is None else run
+        r = _CliRunnerInTarget(run, self._get_target(target))
+        r.set_cli(self._get_cli())
+        return r
+
+    def _get_target(self, target):
+        return self._create_default_target(target)
+
+    def _create_default_target(self, target):
+        t = _DefaultTarget()
+        t.set_target(self._get_envtarget(target))
+        return t
+
+    def _get_envtarget(self, target):
+        return target if self._envname is None else '{target}.{envname}'.format(
+            target=target,
+            envname=self._envname)
+
+    @classmethod
+    def _get_cli(cls):
+        return cls.__name__.lower()
+
+    def _run(self, cmd, target):
+        with self._error_handling(cmd, target):
+            return self._remotesession.execute_command_in_target(
+                cmd, target=target)
+
+    def _nohup_run(self, cmd, target):
+        runner = self._remotesession.get_remoterunner()
+        with self._error_handling(cmd, target):
+            return runner.execute_nohup_background_in_target(cmd,
+                                                             target=target)
+
+    @staticmethod
+    @contextmanager
+    def _error_handling(cmd, target):
+        try:
+            yield None
+        except Exception as e:  # pylint: disable=broad-except
+            raise OpenStackCliError(
+                "{failed_msg}: {ecls}: {e}".format(
+                    failed_msg=_FailedMessage(cmd, target),
+                    ecls=e.__class__.__name__,
+                    e=e))
+
+
+class Runner(OpenStack):
+    """The same as OpenStack_ but the remote command is executed withou prefix.
+
+    **Note:**
+
+    The Robot documentation of keywords is for *openstack* only.
+
+    .. _OpenStack:  crl.openstack.OpenStack.html
+    """
+
+    @staticmethod
+    def _get_cli():
+        return ''