blob: fce80cc0c8ddfcd378b6ba63249bb040529c9b5a [file] [log] [blame]
Dennis Dmitriev6f59add2016-10-18 13:45:27 +03001# Copyright 2016 Mirantis, Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# 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, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
Tatyana Leontovichab47e162017-10-06 16:53:30 +030015import os
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030016import random
Artem Panchenkodb0a97f2017-06-27 19:09:13 +030017import StringIO
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030018
19from devops.helpers import helpers
20from devops.helpers import ssh_client
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030021from devops.helpers import subprocess_runner
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030022from paramiko import rsakey
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +030023import yaml
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030024
25from tcp_tests import logger
Tatyana Leontovichab47e162017-10-06 16:53:30 +030026from tcp_tests.helpers import ext
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030027from tcp_tests.helpers import utils
28
29LOG = logger.logger
30
31
32class UnderlaySSHManager(object):
33 """Keep the list of SSH access credentials to Underlay nodes.
34
35 This object is initialized using config.underlay.ssh.
36
37 :param config_ssh: JSONList of SSH access credentials for nodes:
38 [
39 {
40 node_name: node1,
41 address_pool: 'public-pool01',
42 host: ,
43 port: ,
44 keys: [],
45 keys_source_host: None,
46 login: ,
47 password: ,
Dennis Dmitriev474e3f72016-10-21 16:46:09 +030048 roles: [],
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030049 },
50 {
51 node_name: node1,
52 address_pool: 'private-pool01',
53 host:
54 port:
55 keys: []
56 keys_source_host: None,
57 login:
58 password:
Dennis Dmitriev474e3f72016-10-21 16:46:09 +030059 roles: [],
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030060 },
61 {
62 node_name: node2,
63 address_pool: 'public-pool01',
64 keys_source_host: node1
65 ...
66 }
67 ,
68 ...
69 ]
70
71 self.node_names(): list of node names registered in underlay.
72 self.remote(): SSHClient object by a node name (w/wo address pool)
73 or by a hostname.
74 """
Dennis Dmitriev2a13a132016-11-04 00:56:23 +020075 __config = None
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030076 config_ssh = None
77 config_lvm = None
78
Dennis Dmitriev2a13a132016-11-04 00:56:23 +020079 def __init__(self, config):
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030080 """Read config.underlay.ssh object
81
82 :param config_ssh: dict
83 """
Dennis Dmitriev2a13a132016-11-04 00:56:23 +020084 self.__config = config
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030085 if self.config_ssh is None:
86 self.config_ssh = []
87
88 if self.config_lvm is None:
89 self.config_lvm = {}
90
Dennis Dmitriev2a13a132016-11-04 00:56:23 +020091 self.add_config_ssh(self.__config.underlay.ssh)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030092
93 def add_config_ssh(self, config_ssh):
94
95 if config_ssh is None:
96 config_ssh = []
97
98 for ssh in config_ssh:
99 ssh_data = {
100 # Required keys:
101 'node_name': ssh['node_name'],
102 'host': ssh['host'],
103 'login': ssh['login'],
104 'password': ssh['password'],
105 # Optional keys:
106 'address_pool': ssh.get('address_pool', None),
107 'port': ssh.get('port', None),
108 'keys': ssh.get('keys', []),
Dennis Dmitriev474e3f72016-10-21 16:46:09 +0300109 'roles': ssh.get('roles', []),
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300110 }
111
112 if 'keys_source_host' in ssh:
113 node_name = ssh['keys_source_host']
114 remote = self.remote(node_name)
115 keys = self.__get_keys(remote)
116 ssh_data['keys'].extend(keys)
117
118 self.config_ssh.append(ssh_data)
119
120 def remove_config_ssh(self, config_ssh):
121 if config_ssh is None:
122 config_ssh = []
123
124 for ssh in config_ssh:
125 ssh_data = {
126 # Required keys:
127 'node_name': ssh['node_name'],
128 'host': ssh['host'],
129 'login': ssh['login'],
130 'password': ssh['password'],
131 # Optional keys:
132 'address_pool': ssh.get('address_pool', None),
133 'port': ssh.get('port', None),
134 'keys': ssh.get('keys', []),
Dennis Dmitriev474e3f72016-10-21 16:46:09 +0300135 'roles': ssh.get('roles', []),
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300136 }
137 self.config_ssh.remove(ssh_data)
138
139 def __get_keys(self, remote):
140 keys = []
141 remote.execute('cd ~')
142 key_string = './.ssh/id_rsa'
143 if remote.exists(key_string):
144 with remote.open(key_string) as f:
145 keys.append(rsakey.RSAKey.from_private_key(f))
146 return keys
147
Tatyana Leontovichecd491d2017-09-13 13:51:12 +0300148 def __ssh_data(self, node_name=None, host=None, address_pool=None,
149 node_role=None):
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300150
151 ssh_data = None
152
153 if host is not None:
154 for ssh in self.config_ssh:
155 if host == ssh['host']:
156 ssh_data = ssh
157 break
158
159 elif node_name is not None:
160 for ssh in self.config_ssh:
161 if node_name == ssh['node_name']:
162 if address_pool is not None:
163 if address_pool == ssh['address_pool']:
164 ssh_data = ssh
165 break
166 else:
167 ssh_data = ssh
Tatyana Leontovichecd491d2017-09-13 13:51:12 +0300168 elif node_role is not None:
169 for ssh in self.config_ssh:
170 if node_role in ssh['roles']:
171 if address_pool is not None:
172 if address_pool == ssh['address_pool']:
173 ssh_data = ssh
174 break
175 else:
176 ssh_data = ssh
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300177 if ssh_data is None:
178 raise Exception('Auth data for node was not found using '
179 'node_name="{}" , host="{}" , address_pool="{}"'
180 .format(node_name, host, address_pool))
181 return ssh_data
182
183 def node_names(self):
184 """Get list of node names registered in config.underlay.ssh"""
185
186 names = [] # List is used to keep the original order of names
187 for ssh in self.config_ssh:
188 if ssh['node_name'] not in names:
189 names.append(ssh['node_name'])
190 return names
191
192 def enable_lvm(self, lvmconfig):
193 """Method for enabling lvm oh hosts in environment
194
195 :param lvmconfig: dict with ids or device' names of lvm storage
196 :raises: devops.error.DevopsCalledProcessError,
197 devops.error.TimeoutError, AssertionError, ValueError
198 """
199 def get_actions(lvm_id):
200 return [
201 "systemctl enable lvm2-lvmetad.service",
202 "systemctl enable lvm2-lvmetad.socket",
203 "systemctl start lvm2-lvmetad.service",
204 "systemctl start lvm2-lvmetad.socket",
205 "pvcreate {} && pvs".format(lvm_id),
206 "vgcreate default {} && vgs".format(lvm_id),
207 "lvcreate -L 1G -T default/pool && lvs",
208 ]
209 lvmpackages = ["lvm2", "liblvm2-dev", "thin-provisioning-tools"]
210 for node_name in self.node_names():
211 lvm = lvmconfig.get(node_name, None)
212 if not lvm:
213 continue
214 if 'id' in lvm:
215 lvmdevice = '/dev/disk/by-id/{}'.format(lvm['id'])
216 elif 'device' in lvm:
217 lvmdevice = '/dev/{}'.format(lvm['device'])
218 else:
219 raise ValueError("Unknown LVM device type")
220 if lvmdevice:
221 self.apt_install_package(
222 packages=lvmpackages, node_name=node_name, verbose=True)
223 for command in get_actions(lvmdevice):
224 self.sudo_check_call(command, node_name=node_name,
225 verbose=True)
226 self.config_lvm = dict(lvmconfig)
227
228 def host_by_node_name(self, node_name, address_pool=None):
229 ssh_data = self.__ssh_data(node_name=node_name,
230 address_pool=address_pool)
231 return ssh_data['host']
232
Tatyana Leontovichecd491d2017-09-13 13:51:12 +0300233 def host_by_node_role(self, node_role, address_pool=None):
234 ssh_data = self.__ssh_data(node_role=node_role,
235 address_pool=address_pool)
236 return ssh_data['host']
237
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300238 def remote(self, node_name=None, host=None, address_pool=None):
239 """Get SSHClient by a node name or hostname.
240
241 One of the following arguments should be specified:
242 - host (str): IP address or hostname. If specified, 'node_name' is
243 ignored.
244 - node_name (str): Name of the node stored to config.underlay.ssh
245 - address_pool (str): optional for node_name.
246 If None, use the first matched node_name.
247 """
248 ssh_data = self.__ssh_data(node_name=node_name, host=host,
249 address_pool=address_pool)
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200250 ssh_auth = ssh_client.SSHAuth(
251 username=ssh_data['login'],
252 password=ssh_data['password'],
253 keys=[rsakey.RSAKey(file_obj=StringIO.StringIO(key))
254 for key in ssh_data['keys']])
255
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300256 return ssh_client.SSHClient(
257 host=ssh_data['host'],
258 port=ssh_data['port'] or 22,
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200259 auth=ssh_auth)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300260
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300261 def local(self):
262 """Get Subprocess instance for local operations like:
263
264 underlay.local.execute(command, verbose=False, timeout=None)
265 underlay.local.check_call(
266 command, verbose=False, timeout=None,
267 error_info=None, expected=None, raise_on_err=True)
268 underlay.local.check_stderr(
269 command, verbose=False, timeout=None,
270 error_info=None, raise_on_err=True)
271 """
272 return subprocess_runner.Subprocess()
273
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300274 def check_call(
275 self, cmd,
276 node_name=None, host=None, address_pool=None,
277 verbose=False, timeout=None,
278 error_info=None,
279 expected=None, raise_on_err=True):
280 """Execute command on the node_name/host and check for exit code
281
282 :type cmd: str
283 :type node_name: str
284 :type host: str
285 :type verbose: bool
286 :type timeout: int
287 :type error_info: str
288 :type expected: list
289 :type raise_on_err: bool
290 :rtype: list stdout
291 :raises: devops.error.DevopsCalledProcessError
292 """
293 remote = self.remote(node_name=node_name, host=host,
294 address_pool=address_pool)
295 return remote.check_call(
296 command=cmd, verbose=verbose, timeout=timeout,
297 error_info=error_info, expected=expected,
298 raise_on_err=raise_on_err)
299
300 def apt_install_package(self, packages=None, node_name=None, host=None,
301 **kwargs):
302 """Method to install packages on ubuntu nodes
303
304 :type packages: list
305 :type node_name: str
306 :type host: str
307 :raises: devops.error.DevopsCalledProcessError,
308 devops.error.TimeoutError, AssertionError, ValueError
309
310 Other params of check_call and sudo_check_call are allowed
311 """
312 expected = kwargs.pop('expected', None)
313 if not packages or not isinstance(packages, list):
314 raise ValueError("packages list should be provided!")
315 install = "apt-get install -y {}".format(" ".join(packages))
316 # Should wait until other 'apt' jobs are finished
317 pgrep_expected = [0, 1]
318 pgrep_command = "pgrep -a -f apt"
319 helpers.wait(
320 lambda: (self.check_call(
321 pgrep_command, expected=pgrep_expected, host=host,
322 node_name=node_name, **kwargs).exit_code == 1
323 ), interval=30, timeout=1200,
324 timeout_msg="Timeout reached while waiting for apt lock"
325 )
326 # Install packages
327 self.sudo_check_call("apt-get update", node_name=node_name, host=host,
328 **kwargs)
329 self.sudo_check_call(install, expected=expected, node_name=node_name,
330 host=host, **kwargs)
331
332 def sudo_check_call(
333 self, cmd,
334 node_name=None, host=None, address_pool=None,
335 verbose=False, timeout=None,
336 error_info=None,
337 expected=None, raise_on_err=True):
338 """Execute command with sudo on node_name/host and check for exit code
339
340 :type cmd: str
341 :type node_name: str
342 :type host: str
343 :type verbose: bool
344 :type timeout: int
345 :type error_info: str
346 :type expected: list
347 :type raise_on_err: bool
348 :rtype: list stdout
349 :raises: devops.error.DevopsCalledProcessError
350 """
351 remote = self.remote(node_name=node_name, host=host,
352 address_pool=address_pool)
353 with remote.get_sudo(remote):
354 return remote.check_call(
355 command=cmd, verbose=verbose, timeout=timeout,
356 error_info=error_info, expected=expected,
357 raise_on_err=raise_on_err)
358
359 def dir_upload(self, host, source, destination):
360 """Upload local directory content to remote host
361
362 :param host: str, remote node name
363 :param source: str, local directory path
364 :param destination: str, local directory path
365 """
366 with self.remote(node_name=host) as remote:
367 remote.upload(source, destination)
368
Dennis Dmitriev0f08d9a2017-12-19 02:27:59 +0200369 def get_random_node(self, node_names=None):
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300370 """Get random node name
371
Dennis Dmitriev0f08d9a2017-12-19 02:27:59 +0200372 :param node_names: list of strings
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300373 :return: str, name of node
374 """
Dennis Dmitriev0f08d9a2017-12-19 02:27:59 +0200375 return random.choice(node_names or self.node_names())
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300376
377 def yaml_editor(self, file_path, node_name=None, host=None,
378 address_pool=None):
379 """Returns an initialized YamlEditor instance for context manager
380
381 Usage (with 'underlay' fixture):
382
383 # Local YAML file
384 with underlay.yaml_editor('/path/to/file') as editor:
385 editor.content[key] = "value"
386
387 # Remote YAML file on TCP host
388 with underlay.yaml_editor('/path/to/file',
389 host=config.tcp.tcp_host) as editor:
390 editor.content[key] = "value"
391 """
392 # Local YAML file
393 if node_name is None and host is None:
394 return utils.YamlEditor(file_path=file_path)
395
396 # Remote YAML file
397 ssh_data = self.__ssh_data(node_name=node_name, host=host,
398 address_pool=address_pool)
399 return utils.YamlEditor(
400 file_path=file_path,
401 host=ssh_data['host'],
402 port=ssh_data['port'] or 22,
403 username=ssh_data['login'],
404 password=ssh_data['password'],
405 private_keys=ssh_data['keys'])
Dennis Dmitriev010f4cd2016-11-01 20:43:51 +0200406
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300407 def read_template(self, file_path):
408 """Read yaml as a jinja template"""
409 options = {
410 'config': self.__config,
411 }
412 template = utils.render_template(file_path, options=options)
413 return yaml.load(template)
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300414
415 def get_logs(self, artifact_name,
416 node_role=ext.UNDERLAY_NODE_ROLES.salt_master):
Dennis Dmitriev21369672018-03-24 14:50:18 +0200417
418 # Prefix each '$' symbol with backslash '\' to disable
419 # early interpolation of environment variables on cfg01 node only
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200420 dump_commands = (
Dennis Dmitriev21369672018-03-24 14:50:18 +0200421 "mkdir /root/\$(hostname -f)/;"
422 "rsync -aruv /var/log/ /root/\$(hostname -f)/;"
423 "dpkg -l > /root/\$(hostname -f)/dump_dpkg_l.txt;"
424 "df -h > /root/\$(hostname -f)/dump_df.txt;"
425 "mount > /root/\$(hostname -f)/dump_mount.txt;"
426 "blkid -o list > /root/\$(hostname -f)/dump_blkid_o_list.txt;"
427 "iptables -t nat -S > /root/\$(hostname -f)/dump_iptables_nat.txt;"
428 "iptables -S > /root/\$(hostname -f)/dump_iptables.txt;"
429 "ps auxwwf > /root/\$(hostname -f)/dump_ps.txt;"
430 "docker images > /root/\$(hostname -f)/dump_docker_images.txt;"
431 "docker ps > /root/\$(hostname -f)/dump_docker_ps.txt;"
Mikhail Ivanov82da3392018-03-15 22:19:26 +0400432 "docker service ls > "
Dennis Dmitriev21369672018-03-24 14:50:18 +0200433 " /root/\$(hostname -f)/dump_docker_services_ls.txt;"
434 "for SERVICE in \$(docker service ls | awk '{ print $2 }'); "
435 " do docker service ps --no-trunc 2>&1 \$SERVICE >> "
436 " /root/\$(hostname -f)/dump_docker_service_ps.txt;"
Dennis Dmitriev01d5e372018-03-15 23:29:29 +0200437 " done;"
Dennis Dmitriev21369672018-03-24 14:50:18 +0200438 "for SERVICE in \$(docker service ls | awk '{ print $2 }'); "
439 " do docker service logs 2>&1 \$SERVICE > "
440 " /root/\$(hostname -f)/dump_docker_service_\${SERVICE}_logs;"
Dennis Dmitriev01d5e372018-03-15 23:29:29 +0200441 " done;"
Dennis Dmitriev21369672018-03-24 14:50:18 +0200442 "vgdisplay > /root/\$(hostname -f)/dump_vgdisplay.txt;"
443 "lvdisplay > /root/\$(hostname -f)/dump_lvdisplay.txt;"
444 "ip a > /root/\$(hostname -f)/dump_ip_a.txt;"
445 "ip r > /root/\$(hostname -f)/dump_ip_r.txt;"
446 "netstat -anp > /root/\$(hostname -f)/dump_netstat.txt;"
447 "brctl show > /root/\$(hostname -f)/dump_brctl_show.txt;"
448 "arp -an > /root/\$(hostname -f)/dump_arp.txt;"
449 "uname -a > /root/\$(hostname -f)/dump_uname_a.txt;"
450 "lsmod > /root/\$(hostname -f)/dump_lsmod.txt;"
451 "cat /proc/interrupts > /root/\$(hostname -f)/dump_interrupts.txt;"
452 "cat /etc/*-release > /root/\$(hostname -f)/dump_release.txt;"
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200453 # OpenStack specific, will fail on other nodes
Dennis Dmitriev21369672018-03-24 14:50:18 +0200454 # "rabbitmqctl report > "
455 # " /root/\$(hostname -f)/dump_rabbitmqctl.txt;"
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300456
Dennis Dmitriev21369672018-03-24 14:50:18 +0200457 # "ceph health > /root/\$(hostname -f)/dump_ceph_health.txt;"
458 # "ceph -s > /root/\$(hostname -f)/dump_ceph_s.txt;"
459 # "ceph osd tree > /root/\$(hostname -f)/dump_ceph_osd_tree.txt;"
Dennis Dmitriev0bc485b2017-12-13 12:49:54 +0200460
Dennis Dmitriev21369672018-03-24 14:50:18 +0200461 # "for ns in \$(ip netns list);"
462 # " do echo Namespace: \${ns}; ip netns exec \${ns} ip a;"
463 # "done > /root/\$(hostname -f)/dump_ip_a_ns.txt;"
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200464
Dennis Dmitriev21369672018-03-24 14:50:18 +0200465 # "for ns in \$(ip netns list);"
466 # " do echo Namespace: \${ns}; ip netns exec \${ns} ip r;"
467 # "done > /root/\$(hostname -f)/dump_ip_r_ns.txt;"
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200468
Dennis Dmitriev21369672018-03-24 14:50:18 +0200469 # "for ns in \$(ip netns list);"
470 # " do echo Namespace: \${ns}; ip netns exec \${ns} netstat -anp;"
471 # "done > /root/\$(hostname -f)/dump_netstat_ns.txt;"
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200472
473 "/usr/bin/haproxy-status.sh > "
Dennis Dmitriev21369672018-03-24 14:50:18 +0200474 " /root/\$(hostname -f)/dump_haproxy.txt;"
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200475
476 # Archive the files
477 "cd /root/; tar --absolute-names --warning=no-file-changed "
Dennis Dmitriev21369672018-03-24 14:50:18 +0200478 " -czf \$(hostname -f).tar.gz ./\$(hostname -f)/;"
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200479 )
480
481 master_host = self.__config.salt.salt_master_host
482 with self.remote(host=master_host) as master:
483 # dump files
484 LOG.info("Archive artifacts on all nodes")
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +0200485 master.check_call('salt "*" cmd.run "{0}"'.format(dump_commands),
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200486 raise_on_err=False)
487
488 # create target dir for archives
489 master.check_call("mkdir /root/dump/")
490
491 # get archived artifacts to the master node
492 for node in self.config_ssh:
493 LOG.info("Getting archived artifacts from the node {0}"
Dennis Dmitriev0bc485b2017-12-13 12:49:54 +0200494 .format(node['node_name']))
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200495 master.check_call("rsync -aruv {0}:/root/*.tar.gz "
496 "/root/dump/".format(node['node_name']),
Dennis Dmitriev8feb2522018-03-28 19:17:04 +0300497 raise_on_err=False,
498 timeout=120)
Dennis Dmitriev0bc485b2017-12-13 12:49:54 +0200499
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200500 destination_name = '/root/{0}_dump.tar.gz'.format(artifact_name)
501 # Archive the artifacts from all nodes
502 master.check_call(
503 'cd /root/dump/;'
504 'tar --absolute-names --warning=no-file-changed -czf '
505 ' {0} ./'.format(destination_name))
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300506
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200507 # Download the artifact to the host
Dennis Dmitriev2d643bc2017-12-04 12:23:47 +0200508 LOG.info("Downloading the artifact {0}".format(destination_name))
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200509 master.download(destination=destination_name, target=os.getcwd())
Dennis Dmitriev2d643bc2017-12-04 12:23:47 +0200510
511 def delayed_call(
512 self, cmd,
513 node_name=None, host=None, address_pool=None,
514 verbose=True, timeout=5,
515 delay_min=None, delay_max=None):
516 """Delayed call of the specified command in background
517
518 :param delay_min: minimum delay in minutes before run
519 the command
520 :param delay_max: maximum delay in minutes before run
521 the command
522 The command will be started at random time in the range
523 from delay_min to delay_max in minutes from 'now'
524 using the command 'at'.
525
526 'now' is rounded to integer by 'at' command, i.e.:
527 now(28 min 59 sec) == 28 min 00 sec.
528
529 So, if delay_min=1 , the command may start in range from
530 1 sec to 60 sec.
531
532 If delay_min and delay_max are None, then the command will
533 be executed in the background right now.
534 """
535 time_min = delay_min or delay_max
536 time_max = delay_max or delay_min
537
538 delay = None
539 if time_min is not None and time_max is not None:
540 delay = random.randint(time_min, time_max)
541
542 delay_str = ''
543 if delay:
544 delay_str = " + {0} min".format(delay)
545
546 delay_cmd = "cat << EOF | at now {0}\n{1}\nEOF".format(delay_str, cmd)
547
548 self.check_call(delay_cmd, node_name=node_name, host=host,
549 address_pool=address_pool, verbose=verbose,
550 timeout=timeout)
551
552 def get_target_node_names(self, target='gtw01.'):
553 """Get all node names which names starts with <target>"""
554 return [node_name for node_name
555 in self.node_names()
556 if node_name.startswith(target)]