Add cloudtaf framework
[ta/cloudtaf.git] / libraries / openstackcli / openstackcli.py
1 # Copyright 2019 Nokia
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 """Test library for running *openstack* in remote system.
16 """
17 import abc
18 import json
19 from collections import namedtuple
20 from contextlib import contextmanager
21 import six
22
23
24 class OpenStackCliError(Exception):
25     """Exception raised in case CLI in the remote system fails.
26     """
27     pass
28
29
30 class _FailedMessage(namedtuple('FailedMessage', ['cmd', 'target'])):
31     def __str__(self):
32         return "Remote command '{cmd}' in target '{target}' failed".format(
33             cmd=self.cmd,
34             target=self.target)
35
36
37 @six.add_metaclass(abc.ABCMeta)
38 class _TargetBase(object):
39
40     @abc.abstractproperty
41     def target(self):
42         """Return *RemoteSession* target where command is to be executed
43         """
44
45     @abc.abstractproperty
46     def cmd_template(self):
47         """Return cmd template with field *cli*, *cmd* and *fmt*.
48         """
49
50
51 class _DefaultTarget(_TargetBase):
52     def __init__(self):
53         self._raw_target = None
54         self._target = None
55         self._cloud = 'default'
56
57     def set_target(self, target):
58         self._raw_target = target
59         self._setup()
60
61     def _setup(self):
62         self._target = self._raw_target
63         if self._partslen == 2 or self._partslen == 3:
64             self._update_target_and_cloud()
65
66     def _update_target_and_cloud(self):
67         if self._oper == 'os-cloud':
68             self._target = self._parts[0] if self._partslen == 3 else 'default'
69             self._cloud = self._parts[-1]
70         elif self._partslen == 2:
71             self._cloud = None
72
73     @property
74     def _oper(self):
75         return self._parts[self._partslen - 2]
76
77     @property
78     def _parts(self):
79         return self._raw_target.split('.')
80
81     @property
82     def _partslen(self):
83         return len(self._parts)
84
85     @property
86     def target(self):
87         return self._target
88
89     @property
90     def cmd_template(self):
91         return ('{cli} {cmd}{fmt}'
92                 if self._cloud is None else
93                 '{{cli}} --os-cloud {cloud} {{cmd}}{{fmt}}'.format(cloud=self._cloud))
94
95
96 class _CliRunnerInTarget(object):
97     def __init__(self, run, target):
98         self._run = run
99         self._target = target
100         self._cli = None
101
102     def set_cli(self, cli):
103         self._cli = cli
104
105     def run_with_json(self, cmd):
106         return self._run_with_verification(cmd, fmt=' -f json')
107
108     def run(self, cmd):
109         return self._run_with_verification(cmd)
110
111     def _run_with_verification(self, cmd, fmt=''):
112         cmd = self._get_formatted_cmd(cmd, fmt)
113         result = self._run(cmd, self._target.target)
114         self._verify_result(result, _FailedMessage(cmd, self._target.target))
115         return result
116
117     def run_without_verification(self, cmd, fmt=''):
118         return self._run(self._get_formatted_cmd(cmd, fmt), self._target.target)
119
120     def _get_formatted_cmd(self, cmd, fmt):
121         return self._target.cmd_template.format(cli=self._cli,
122                                                 cmd=cmd,
123                                                 fmt=fmt)
124
125     def _verify_result(self, result, failedmessage):
126         status = self._get_integer_status(result, failedmessage)
127         if status or result.stderr:
128             self._raise_openstackerror(failedmessage, result)
129         return result
130
131     def _get_integer_status(self, result, failedmessage):
132         try:
133             return int(result.status)
134         except ValueError:
135             self._raise_openstackerror(failedmessage, result)
136
137     @staticmethod
138     def _raise_openstackerror(failedmessage, result):
139         raise OpenStackCliError(
140             '{failedmessage}: status: {status!r}, '
141             'stdout: {stdout!r}, '
142             'stderr: {stderr!r}'.format(failedmessage=failedmessage,
143                                         status=result.status,
144                                         stdout=result.stdout,
145                                         stderr=result.stderr))
146
147
148 class OpenStack(object):
149     """Remote *openstack* runner.
150     The runner executes commands in *crl.remotesession* target but the target
151     *os-cloud.cloudconfigname* is interpreted to *--os-cloud cloudconfigname*
152     argument and executed in the *default* target. Respectively, the target
153     *controller-1.os-cloud.cloudconfigname* is executed in *controller-1*
154     target with *--os-cloud cloudconfigname*.
155
156     If the *target* is of form *target.envname*, then *--os-cloud* is not given
157     as *crl.remotesession* environment handling should then take care of
158     setting the correct environment for the *envname* in *target*.
159     """
160
161     def __init__(self):
162         self._remotesession = None
163         self._envname = None
164         self._openrc_tuples = {}
165
166     def initialize(self, remotesession, envname=None):
167         """Initialize the library.
168
169         Args:
170             remotesession: `crl.remotesession.remotesession.RemoteSession`_
171                            instance
172
173             envname: Environment name appended to the target in form target.envname.
174                      See Robot example 2 for details of the usage.
175
176         **Note:**
177
178            Targets are interpreted in the following manner:
179
180            ============================== ==================== ====================
181            Target                         RemoteSession target os-cloud config name
182            ============================== ==================== ====================
183            os-cloud.confname              default              confname
184            controller-1.os-cloud.confname controller-1         confname
185            target.envname                 target.envname       None
186            ============================== ==================== ====================
187
188         **Robot Examples**
189
190             In the following example is assumed that
191             `crl.remotesession.remotesession.RemoteSession`_ is imported in
192             Library settings *WITH NAME* RemoteSession.
193
194
195         *Example 1*
196
197             ====================   =====================  =============
198             ${remotesession}=      Get Library Instance   RemoteSession
199             OpenStack.Initialize   ${remotesession}
200             ====================   =====================  =============
201
202         *Example 2*
203
204             If in the test setup initialization is done with given *myenv* environment
205
206             ====================   =====================  =============
207             ${remotesession}=      Get Library Instance   RemoteSession
208             OpenStack.Initialize   ${remotesession}       envname=myenv
209             ====================   =====================  =============
210
211             Then
212
213             =============  ==========  =============
214             OpenStack.Run  quota show  target=target
215             =============  ==========  =============
216
217             runs *openstack quota show* in the target *target.myenv*.
218
219         .. _crl.remotesession.remotesession.RemoteSession:
220            https://crl-remotesession.readthedocs.io/en/latest
221            /crl.remotesession.remotesession.RemoteSession.html
222         """
223         self._remotesession = remotesession
224         self._envname = envname
225
226     def run(self, cmd, target='default'):
227         """ Run *openstack* in the remote target with *json* format.
228
229         Args:
230             cmd: openstack command to be executed in target.
231
232             target: `crl.remotesession.remotesession.RemoteSession`_ target.
233
234         Returns:
235             Decoded *json* formatted command output. For example in case
236             openstack output (
237             for command *openstack.run('quota show')* is in remote::
238
239                 # openstack --os-cloud default quota show -f json
240                 {
241                  "secgroups": 10,
242                  "health_monitors": null,
243                  "l7_policies": null,
244                  ...
245                 }
246
247             then the return value of *run* is:
248
249             .. code-block:: python
250
251                 {
252                   "secgroups": 10,
253                   "health_monitors": None,
254                   "17_policies": None,
255                   ...
256                 }
257
258         **Robot Example:**
259
260             ================ ===================== ===========
261             ${quota}=        OpenStack.Run         quota show
262             Should Be Equal  ${quota['secgroups']} 10
263             ================ ===================== ===========
264
265         Raises:
266             OpenStackCliError: if remote *openstack* fails.
267
268         .. _crl.remotesession.remotesession.RemoteSession:
269            https://crl-remotesession.readthedocs.io/en/latest
270            /crl.remotesession.remotesession.RemoteSession.html
271         """
272         result = self._create_runner(target).run_with_json(cmd)
273         with self._error_handling(cmd, target):
274             return json.loads(result.stdout)
275
276     def run_ignore_output(self, cmd, target='default'):
277         """ Run *openstack* in the remote target and ignore the output.
278
279         Args:
280             cmd: openstack command to be executed in target.
281
282             target: `crl.remotesession.remotesession.RemoteSession`_ target.
283
284         Returns:
285             Nothing
286
287         Raises:
288             OpenStackCliError: if remote *openstack* fails.
289
290         .. _crl.remotesession.remotesession.RemoteSession:
291            https://crl-remotesession.readthedocs.io/en/latest
292            /crl.remotesession.remotesession.RemoteSession.html
293         """
294         self._create_runner(target).run(cmd)
295
296     def run_raw(self, cmd, target='default'):
297         """ Run *openstack* in the remote target and return raw output without
298         formatting.
299
300         Args:
301             cmd: openstack command to be executed in target.
302
303             target: `crl.remotesession.remotesession.RemoteSession`_ target.
304
305         Returns:
306             Nothing
307
308         Raises:
309             OpenStackCliError: if remote *openstack* fails.
310
311         .. _crl.remotesession.remotesession.RemoteSession:
312            https://crl-remotesession.readthedocs.io/en/latest
313            /crl.remotesession.remotesession.RemoteSession.html
314         """
315         return self._create_runner(target).run(cmd).stdout
316
317     def run_nohup(self, cmd, target='default'):
318         """ Run *openstack* in the remote target nohup mode in background and
319         return PID of the started process.
320
321         Note:
322             This keyword is available only for targets initialized by
323             *Set Runner Target* keyword of *RemoteSession*. The version of
324             crl.interactivesessions must be at least as new as
325             crl.interactivesessions==1.0b4.
326
327         Args:
328             cmd: openstack command to be executed in target.
329
330             target: `crl.remotesession.remotesession.RemoteSession`_ target.
331
332         Returns:
333             PID of the process running *cmd* in the target.
334
335         Raises:
336             OpenStackCliError: if remote *openstack* fails.
337
338         .. _crl.remotesession.remotesession.RemoteSession:
339            https://crl-remotesession.readthedocs.io/en/latest
340            /crl.remotesession.remotesession.RemoteSession.html
341         """
342         return self._create_runner(
343             target, run=self._nohup_run).run_without_verification(cmd)
344
345     def _create_runner(self, target, run=None):
346         run = self._run if run is None else run
347         r = _CliRunnerInTarget(run, self._get_target(target))
348         r.set_cli(self._get_cli())
349         return r
350
351     def _get_target(self, target):
352         return self._create_default_target(target)
353
354     def _create_default_target(self, target):
355         t = _DefaultTarget()
356         t.set_target(self._get_envtarget(target))
357         return t
358
359     def _get_envtarget(self, target):
360         return target if self._envname is None else '{target}.{envname}'.format(
361             target=target,
362             envname=self._envname)
363
364     @classmethod
365     def _get_cli(cls):
366         return cls.__name__.lower()
367
368     def _run(self, cmd, target):
369         with self._error_handling(cmd, target):
370             return self._remotesession.execute_command_in_target(
371                 cmd, target=target)
372
373     def _nohup_run(self, cmd, target):
374         runner = self._remotesession.get_remoterunner()
375         with self._error_handling(cmd, target):
376             return runner.execute_nohup_background_in_target(cmd,
377                                                              target=target)
378
379     @staticmethod
380     @contextmanager
381     def _error_handling(cmd, target):
382         try:
383             yield None
384         except Exception as e:  # pylint: disable=broad-except
385             raise OpenStackCliError(
386                 "{failed_msg}: {ecls}: {e}".format(
387                     failed_msg=_FailedMessage(cmd, target),
388                     ecls=e.__class__.__name__,
389                     e=e))
390
391
392 class Runner(OpenStack):
393     """The same as OpenStack_ but the remote command is executed withou prefix.
394
395     **Note:**
396
397     The Robot documentation of keywords is for *openstack* only.
398
399     .. _OpenStack:  crl.openstack.OpenStack.html
400     """
401
402     @staticmethod
403     def _get_cli():
404         return ''