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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
15 """Test library for running *openstack* in remote system.
19 from collections import namedtuple
20 from contextlib import contextmanager
24 class OpenStackCliError(Exception):
25 """Exception raised in case CLI in the remote system fails.
30 class _FailedMessage(namedtuple('FailedMessage', ['cmd', 'target'])):
32 return "Remote command '{cmd}' in target '{target}' failed".format(
37 @six.add_metaclass(abc.ABCMeta)
38 class _TargetBase(object):
42 """Return *RemoteSession* target where command is to be executed
46 def cmd_template(self):
47 """Return cmd template with field *cli*, *cmd* and *fmt*.
51 class _DefaultTarget(_TargetBase):
53 self._raw_target = None
55 self._cloud = 'default'
57 def set_target(self, target):
58 self._raw_target = target
62 self._target = self._raw_target
63 if self._partslen == 2 or self._partslen == 3:
64 self._update_target_and_cloud()
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:
75 return self._parts[self._partslen - 2]
79 return self._raw_target.split('.')
83 return len(self._parts)
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))
96 class _CliRunnerInTarget(object):
97 def __init__(self, run, target):
102 def set_cli(self, cli):
105 def run_with_json(self, cmd):
106 return self._run_with_verification(cmd, fmt=' -f json')
109 return self._run_with_verification(cmd)
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))
117 def run_without_verification(self, cmd, fmt=''):
118 return self._run(self._get_formatted_cmd(cmd, fmt), self._target.target)
120 def _get_formatted_cmd(self, cmd, fmt):
121 return self._target.cmd_template.format(cli=self._cli,
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)
131 def _get_integer_status(self, result, failedmessage):
133 return int(result.status)
135 self._raise_openstackerror(failedmessage, result)
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))
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*.
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*.
162 self._remotesession = None
164 self._openrc_tuples = {}
166 def initialize(self, remotesession, envname=None):
167 """Initialize the library.
170 remotesession: `crl.remotesession.remotesession.RemoteSession`_
173 envname: Environment name appended to the target in form target.envname.
174 See Robot example 2 for details of the usage.
178 Targets are interpreted in the following manner:
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 ============================== ==================== ====================
190 In the following example is assumed that
191 `crl.remotesession.remotesession.RemoteSession`_ is imported in
192 Library settings *WITH NAME* RemoteSession.
197 ==================== ===================== =============
198 ${remotesession}= Get Library Instance RemoteSession
199 OpenStack.Initialize ${remotesession}
200 ==================== ===================== =============
204 If in the test setup initialization is done with given *myenv* environment
206 ==================== ===================== =============
207 ${remotesession}= Get Library Instance RemoteSession
208 OpenStack.Initialize ${remotesession} envname=myenv
209 ==================== ===================== =============
213 ============= ========== =============
214 OpenStack.Run quota show target=target
215 ============= ========== =============
217 runs *openstack quota show* in the target *target.myenv*.
219 .. _crl.remotesession.remotesession.RemoteSession:
220 https://crl-remotesession.readthedocs.io/en/latest
221 /crl.remotesession.remotesession.RemoteSession.html
223 self._remotesession = remotesession
224 self._envname = envname
226 def run(self, cmd, target='default'):
227 """ Run *openstack* in the remote target with *json* format.
230 cmd: openstack command to be executed in target.
232 target: `crl.remotesession.remotesession.RemoteSession`_ target.
235 Decoded *json* formatted command output. For example in case
237 for command *openstack.run('quota show')* is in remote::
239 # openstack --os-cloud default quota show -f json
242 "health_monitors": null,
247 then the return value of *run* is:
249 .. code-block:: python
253 "health_monitors": None,
260 ================ ===================== ===========
261 ${quota}= OpenStack.Run quota show
262 Should Be Equal ${quota['secgroups']} 10
263 ================ ===================== ===========
266 OpenStackCliError: if remote *openstack* fails.
268 .. _crl.remotesession.remotesession.RemoteSession:
269 https://crl-remotesession.readthedocs.io/en/latest
270 /crl.remotesession.remotesession.RemoteSession.html
272 result = self._create_runner(target).run_with_json(cmd)
273 with self._error_handling(cmd, target):
274 return json.loads(result.stdout)
276 def run_ignore_output(self, cmd, target='default'):
277 """ Run *openstack* in the remote target and ignore the output.
280 cmd: openstack command to be executed in target.
282 target: `crl.remotesession.remotesession.RemoteSession`_ target.
288 OpenStackCliError: if remote *openstack* fails.
290 .. _crl.remotesession.remotesession.RemoteSession:
291 https://crl-remotesession.readthedocs.io/en/latest
292 /crl.remotesession.remotesession.RemoteSession.html
294 self._create_runner(target).run(cmd)
296 def run_raw(self, cmd, target='default'):
297 """ Run *openstack* in the remote target and return raw output without
301 cmd: openstack command to be executed in target.
303 target: `crl.remotesession.remotesession.RemoteSession`_ target.
309 OpenStackCliError: if remote *openstack* fails.
311 .. _crl.remotesession.remotesession.RemoteSession:
312 https://crl-remotesession.readthedocs.io/en/latest
313 /crl.remotesession.remotesession.RemoteSession.html
315 return self._create_runner(target).run(cmd).stdout
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.
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.
328 cmd: openstack command to be executed in target.
330 target: `crl.remotesession.remotesession.RemoteSession`_ target.
333 PID of the process running *cmd* in the target.
336 OpenStackCliError: if remote *openstack* fails.
338 .. _crl.remotesession.remotesession.RemoteSession:
339 https://crl-remotesession.readthedocs.io/en/latest
340 /crl.remotesession.remotesession.RemoteSession.html
342 return self._create_runner(
343 target, run=self._nohup_run).run_without_verification(cmd)
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())
351 def _get_target(self, target):
352 return self._create_default_target(target)
354 def _create_default_target(self, target):
356 t.set_target(self._get_envtarget(target))
359 def _get_envtarget(self, target):
360 return target if self._envname is None else '{target}.{envname}'.format(
362 envname=self._envname)
366 return cls.__name__.lower()
368 def _run(self, cmd, target):
369 with self._error_handling(cmd, target):
370 return self._remotesession.execute_command_in_target(
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,
381 def _error_handling(cmd, target):
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__,
392 class Runner(OpenStack):
393 """The same as OpenStack_ but the remote command is executed withou prefix.
397 The Robot documentation of keywords is for *openstack* only.
399 .. _OpenStack: crl.openstack.OpenStack.html