X-Git-Url: https://gerrit.akraino.org/r/gitweb?p=ta%2Fcloudtaf.git;a=blobdiff_plain;f=libraries%2Fopenstackcli%2Fopenstackcli.py;fp=libraries%2Fopenstackcli%2Fopenstackcli.py;h=72559ca891207005e68986e01b7a6851760f0796;hp=0000000000000000000000000000000000000000;hb=d448b9388fd9cb3732e35996b98f493a5a5921d4;hpb=07c5f13d2429236a603c867e09c4cc3b42e75826 diff --git a/libraries/openstackcli/openstackcli.py b/libraries/openstackcli/openstackcli.py new file mode 100644 index 0000000..72559ca --- /dev/null +++ b/libraries/openstackcli/openstackcli.py @@ -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 ''