# 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 ''