blob: 0bfb46303d0f653a9e9a4885b2bd2a0b953966e9 [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
Dennis Dmitrievd2604512018-06-04 05:34:44 +030026from tcp_tests import settings
Tatyana Leontovichab47e162017-10-06 16:53:30 +030027from tcp_tests.helpers import ext
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030028from tcp_tests.helpers import utils
29
30LOG = logger.logger
31
32
33class UnderlaySSHManager(object):
34 """Keep the list of SSH access credentials to Underlay nodes.
35
36 This object is initialized using config.underlay.ssh.
37
38 :param config_ssh: JSONList of SSH access credentials for nodes:
39 [
40 {
41 node_name: node1,
42 address_pool: 'public-pool01',
43 host: ,
44 port: ,
45 keys: [],
46 keys_source_host: None,
47 login: ,
48 password: ,
Dennis Dmitriev474e3f72016-10-21 16:46:09 +030049 roles: [],
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030050 },
51 {
52 node_name: node1,
53 address_pool: 'private-pool01',
54 host:
55 port:
56 keys: []
57 keys_source_host: None,
58 login:
59 password:
Dennis Dmitriev474e3f72016-10-21 16:46:09 +030060 roles: [],
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030061 },
62 {
63 node_name: node2,
64 address_pool: 'public-pool01',
65 keys_source_host: node1
66 ...
67 }
68 ,
69 ...
70 ]
71
72 self.node_names(): list of node names registered in underlay.
73 self.remote(): SSHClient object by a node name (w/wo address pool)
74 or by a hostname.
75 """
Dennis Dmitriev2a13a132016-11-04 00:56:23 +020076 __config = None
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030077 config_ssh = None
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030078
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
Dennis Dmitriev2a13a132016-11-04 00:56:23 +020088 self.add_config_ssh(self.__config.underlay.ssh)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030089
90 def add_config_ssh(self, config_ssh):
91
92 if config_ssh is None:
93 config_ssh = []
94
95 for ssh in config_ssh:
96 ssh_data = {
97 # Required keys:
98 'node_name': ssh['node_name'],
99 'host': ssh['host'],
100 'login': ssh['login'],
101 'password': ssh['password'],
102 # Optional keys:
103 'address_pool': ssh.get('address_pool', None),
104 'port': ssh.get('port', None),
105 'keys': ssh.get('keys', []),
Dennis Dmitriev474e3f72016-10-21 16:46:09 +0300106 'roles': ssh.get('roles', []),
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300107 }
108
109 if 'keys_source_host' in ssh:
110 node_name = ssh['keys_source_host']
111 remote = self.remote(node_name)
112 keys = self.__get_keys(remote)
113 ssh_data['keys'].extend(keys)
114
115 self.config_ssh.append(ssh_data)
116
117 def remove_config_ssh(self, config_ssh):
118 if config_ssh is None:
119 config_ssh = []
120
121 for ssh in config_ssh:
122 ssh_data = {
123 # Required keys:
124 'node_name': ssh['node_name'],
125 'host': ssh['host'],
126 'login': ssh['login'],
127 'password': ssh['password'],
128 # Optional keys:
129 'address_pool': ssh.get('address_pool', None),
130 'port': ssh.get('port', None),
131 'keys': ssh.get('keys', []),
Dennis Dmitriev474e3f72016-10-21 16:46:09 +0300132 'roles': ssh.get('roles', []),
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300133 }
134 self.config_ssh.remove(ssh_data)
135
136 def __get_keys(self, remote):
137 keys = []
138 remote.execute('cd ~')
139 key_string = './.ssh/id_rsa'
140 if remote.exists(key_string):
141 with remote.open(key_string) as f:
142 keys.append(rsakey.RSAKey.from_private_key(f))
143 return keys
144
Tatyana Leontovichecd491d2017-09-13 13:51:12 +0300145 def __ssh_data(self, node_name=None, host=None, address_pool=None,
146 node_role=None):
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300147
148 ssh_data = None
149
150 if host is not None:
151 for ssh in self.config_ssh:
152 if host == ssh['host']:
153 ssh_data = ssh
154 break
155
156 elif node_name is not None:
157 for ssh in self.config_ssh:
158 if node_name == ssh['node_name']:
159 if address_pool is not None:
160 if address_pool == ssh['address_pool']:
161 ssh_data = ssh
162 break
163 else:
164 ssh_data = ssh
Tatyana Leontovichecd491d2017-09-13 13:51:12 +0300165 elif node_role is not None:
166 for ssh in self.config_ssh:
167 if node_role in ssh['roles']:
168 if address_pool is not None:
169 if address_pool == ssh['address_pool']:
170 ssh_data = ssh
171 break
172 else:
173 ssh_data = ssh
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300174 if ssh_data is None:
Dmitry Tyzhnenkob610afd2018-02-19 15:43:45 +0200175 LOG.debug("config_ssh - {}".format(self.config_ssh))
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300176 raise Exception('Auth data for node was not found using '
177 'node_name="{}" , host="{}" , address_pool="{}"'
178 .format(node_name, host, address_pool))
179 return ssh_data
180
181 def node_names(self):
182 """Get list of node names registered in config.underlay.ssh"""
183
184 names = [] # List is used to keep the original order of names
185 for ssh in self.config_ssh:
186 if ssh['node_name'] not in names:
187 names.append(ssh['node_name'])
188 return names
189
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300190 def host_by_node_name(self, node_name, address_pool=None):
191 ssh_data = self.__ssh_data(node_name=node_name,
192 address_pool=address_pool)
193 return ssh_data['host']
194
Tatyana Leontovichecd491d2017-09-13 13:51:12 +0300195 def host_by_node_role(self, node_role, address_pool=None):
196 ssh_data = self.__ssh_data(node_role=node_role,
197 address_pool=address_pool)
198 return ssh_data['host']
199
Dmitry Tyzhnenko35413c02018-03-05 14:12:37 +0200200 def remote(self, node_name=None, host=None, address_pool=None,
201 username=None):
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300202 """Get SSHClient by a node name or hostname.
203
204 One of the following arguments should be specified:
205 - host (str): IP address or hostname. If specified, 'node_name' is
206 ignored.
207 - node_name (str): Name of the node stored to config.underlay.ssh
208 - address_pool (str): optional for node_name.
209 If None, use the first matched node_name.
210 """
211 ssh_data = self.__ssh_data(node_name=node_name, host=host,
212 address_pool=address_pool)
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200213 ssh_auth = ssh_client.SSHAuth(
Dmitry Tyzhnenko35413c02018-03-05 14:12:37 +0200214 username=username or ssh_data['login'],
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200215 password=ssh_data['password'],
216 keys=[rsakey.RSAKey(file_obj=StringIO.StringIO(key))
217 for key in ssh_data['keys']])
218
Dennis Dmitrievd2604512018-06-04 05:34:44 +0300219 client = ssh_client.SSHClient(
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300220 host=ssh_data['host'],
221 port=ssh_data['port'] or 22,
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200222 auth=ssh_auth)
Dennis Dmitrievd2604512018-06-04 05:34:44 +0300223 client._ssh.get_transport().set_keepalive(
224 settings.SSH_SERVER_ALIVE_INTERVAL)
225
226 return client
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300227
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300228 def local(self):
229 """Get Subprocess instance for local operations like:
230
231 underlay.local.execute(command, verbose=False, timeout=None)
232 underlay.local.check_call(
233 command, verbose=False, timeout=None,
234 error_info=None, expected=None, raise_on_err=True)
235 underlay.local.check_stderr(
236 command, verbose=False, timeout=None,
237 error_info=None, raise_on_err=True)
238 """
239 return subprocess_runner.Subprocess()
240
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300241 def check_call(
242 self, cmd,
243 node_name=None, host=None, address_pool=None,
244 verbose=False, timeout=None,
245 error_info=None,
246 expected=None, raise_on_err=True):
247 """Execute command on the node_name/host and check for exit code
248
249 :type cmd: str
250 :type node_name: str
251 :type host: str
252 :type verbose: bool
253 :type timeout: int
254 :type error_info: str
255 :type expected: list
256 :type raise_on_err: bool
257 :rtype: list stdout
258 :raises: devops.error.DevopsCalledProcessError
259 """
260 remote = self.remote(node_name=node_name, host=host,
261 address_pool=address_pool)
262 return remote.check_call(
263 command=cmd, verbose=verbose, timeout=timeout,
264 error_info=error_info, expected=expected,
265 raise_on_err=raise_on_err)
266
267 def apt_install_package(self, packages=None, node_name=None, host=None,
268 **kwargs):
269 """Method to install packages on ubuntu nodes
270
271 :type packages: list
272 :type node_name: str
273 :type host: str
274 :raises: devops.error.DevopsCalledProcessError,
275 devops.error.TimeoutError, AssertionError, ValueError
276
277 Other params of check_call and sudo_check_call are allowed
278 """
279 expected = kwargs.pop('expected', None)
280 if not packages or not isinstance(packages, list):
281 raise ValueError("packages list should be provided!")
282 install = "apt-get install -y {}".format(" ".join(packages))
283 # Should wait until other 'apt' jobs are finished
284 pgrep_expected = [0, 1]
285 pgrep_command = "pgrep -a -f apt"
286 helpers.wait(
287 lambda: (self.check_call(
288 pgrep_command, expected=pgrep_expected, host=host,
289 node_name=node_name, **kwargs).exit_code == 1
290 ), interval=30, timeout=1200,
291 timeout_msg="Timeout reached while waiting for apt lock"
292 )
293 # Install packages
294 self.sudo_check_call("apt-get update", node_name=node_name, host=host,
295 **kwargs)
296 self.sudo_check_call(install, expected=expected, node_name=node_name,
297 host=host, **kwargs)
298
299 def sudo_check_call(
300 self, cmd,
301 node_name=None, host=None, address_pool=None,
302 verbose=False, timeout=None,
303 error_info=None,
304 expected=None, raise_on_err=True):
305 """Execute command with sudo on node_name/host and check for exit code
306
307 :type cmd: str
308 :type node_name: str
309 :type host: str
310 :type verbose: bool
311 :type timeout: int
312 :type error_info: str
313 :type expected: list
314 :type raise_on_err: bool
315 :rtype: list stdout
316 :raises: devops.error.DevopsCalledProcessError
317 """
318 remote = self.remote(node_name=node_name, host=host,
319 address_pool=address_pool)
Dennis Dmitriev3ec2e532018-06-08 04:33:34 +0300320 with remote.sudo(enforce=True):
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300321 return remote.check_call(
322 command=cmd, verbose=verbose, timeout=timeout,
323 error_info=error_info, expected=expected,
324 raise_on_err=raise_on_err)
325
326 def dir_upload(self, host, source, destination):
327 """Upload local directory content to remote host
328
329 :param host: str, remote node name
330 :param source: str, local directory path
331 :param destination: str, local directory path
332 """
333 with self.remote(node_name=host) as remote:
334 remote.upload(source, destination)
335
Dennis Dmitriev0f08d9a2017-12-19 02:27:59 +0200336 def get_random_node(self, node_names=None):
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300337 """Get random node name
338
Dennis Dmitriev0f08d9a2017-12-19 02:27:59 +0200339 :param node_names: list of strings
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300340 :return: str, name of node
341 """
Dennis Dmitriev0f08d9a2017-12-19 02:27:59 +0200342 return random.choice(node_names or self.node_names())
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300343
344 def yaml_editor(self, file_path, node_name=None, host=None,
345 address_pool=None):
346 """Returns an initialized YamlEditor instance for context manager
347
348 Usage (with 'underlay' fixture):
349
350 # Local YAML file
351 with underlay.yaml_editor('/path/to/file') as editor:
352 editor.content[key] = "value"
353
354 # Remote YAML file on TCP host
355 with underlay.yaml_editor('/path/to/file',
356 host=config.tcp.tcp_host) as editor:
357 editor.content[key] = "value"
358 """
359 # Local YAML file
360 if node_name is None and host is None:
361 return utils.YamlEditor(file_path=file_path)
362
363 # Remote YAML file
364 ssh_data = self.__ssh_data(node_name=node_name, host=host,
365 address_pool=address_pool)
366 return utils.YamlEditor(
367 file_path=file_path,
368 host=ssh_data['host'],
369 port=ssh_data['port'] or 22,
370 username=ssh_data['login'],
371 password=ssh_data['password'],
372 private_keys=ssh_data['keys'])
Dennis Dmitriev010f4cd2016-11-01 20:43:51 +0200373
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300374 def read_template(self, file_path):
375 """Read yaml as a jinja template"""
376 options = {
377 'config': self.__config,
378 }
379 template = utils.render_template(file_path, options=options)
380 return yaml.load(template)
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300381
382 def get_logs(self, artifact_name,
383 node_role=ext.UNDERLAY_NODE_ROLES.salt_master):
Dennis Dmitriev21369672018-03-24 14:50:18 +0200384
385 # Prefix each '$' symbol with backslash '\' to disable
386 # early interpolation of environment variables on cfg01 node only
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200387 dump_commands = (
Dennis Dmitriev21369672018-03-24 14:50:18 +0200388 "mkdir /root/\$(hostname -f)/;"
389 "rsync -aruv /var/log/ /root/\$(hostname -f)/;"
390 "dpkg -l > /root/\$(hostname -f)/dump_dpkg_l.txt;"
391 "df -h > /root/\$(hostname -f)/dump_df.txt;"
392 "mount > /root/\$(hostname -f)/dump_mount.txt;"
393 "blkid -o list > /root/\$(hostname -f)/dump_blkid_o_list.txt;"
394 "iptables -t nat -S > /root/\$(hostname -f)/dump_iptables_nat.txt;"
395 "iptables -S > /root/\$(hostname -f)/dump_iptables.txt;"
396 "ps auxwwf > /root/\$(hostname -f)/dump_ps.txt;"
397 "docker images > /root/\$(hostname -f)/dump_docker_images.txt;"
398 "docker ps > /root/\$(hostname -f)/dump_docker_ps.txt;"
Mikhail Ivanov82da3392018-03-15 22:19:26 +0400399 "docker service ls > "
Dennis Dmitriev21369672018-03-24 14:50:18 +0200400 " /root/\$(hostname -f)/dump_docker_services_ls.txt;"
Dennis Dmitrievefe5c0b2018-10-24 20:35:26 +0300401 "for SERVICE in \$(docker service ls | awk '{ print \$2 }'); "
Dennis Dmitriev21369672018-03-24 14:50:18 +0200402 " do docker service ps --no-trunc 2>&1 \$SERVICE >> "
403 " /root/\$(hostname -f)/dump_docker_service_ps.txt;"
Dennis Dmitriev01d5e372018-03-15 23:29:29 +0200404 " done;"
Dennis Dmitrievefe5c0b2018-10-24 20:35:26 +0300405 "for SERVICE in \$(docker service ls | awk '{ print \$2 }'); "
406 " do timeout 30 docker service logs --no-trunc 2>&1 \$SERVICE > "
Dennis Dmitriev21369672018-03-24 14:50:18 +0200407 " /root/\$(hostname -f)/dump_docker_service_\${SERVICE}_logs;"
Dennis Dmitriev01d5e372018-03-15 23:29:29 +0200408 " done;"
Dennis Dmitriev21369672018-03-24 14:50:18 +0200409 "vgdisplay > /root/\$(hostname -f)/dump_vgdisplay.txt;"
410 "lvdisplay > /root/\$(hostname -f)/dump_lvdisplay.txt;"
411 "ip a > /root/\$(hostname -f)/dump_ip_a.txt;"
412 "ip r > /root/\$(hostname -f)/dump_ip_r.txt;"
413 "netstat -anp > /root/\$(hostname -f)/dump_netstat.txt;"
414 "brctl show > /root/\$(hostname -f)/dump_brctl_show.txt;"
415 "arp -an > /root/\$(hostname -f)/dump_arp.txt;"
416 "uname -a > /root/\$(hostname -f)/dump_uname_a.txt;"
417 "lsmod > /root/\$(hostname -f)/dump_lsmod.txt;"
418 "cat /proc/interrupts > /root/\$(hostname -f)/dump_interrupts.txt;"
419 "cat /etc/*-release > /root/\$(hostname -f)/dump_release.txt;"
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200420 # OpenStack specific, will fail on other nodes
Dennis Dmitriev21369672018-03-24 14:50:18 +0200421 # "rabbitmqctl report > "
422 # " /root/\$(hostname -f)/dump_rabbitmqctl.txt;"
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300423
Dennis Dmitriev21369672018-03-24 14:50:18 +0200424 # "ceph health > /root/\$(hostname -f)/dump_ceph_health.txt;"
425 # "ceph -s > /root/\$(hostname -f)/dump_ceph_s.txt;"
426 # "ceph osd tree > /root/\$(hostname -f)/dump_ceph_osd_tree.txt;"
Dennis Dmitriev0bc485b2017-12-13 12:49:54 +0200427
Dennis Dmitriev21369672018-03-24 14:50:18 +0200428 # "for ns in \$(ip netns list);"
429 # " do echo Namespace: \${ns}; ip netns exec \${ns} ip a;"
430 # "done > /root/\$(hostname -f)/dump_ip_a_ns.txt;"
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200431
Dennis Dmitriev21369672018-03-24 14:50:18 +0200432 # "for ns in \$(ip netns list);"
433 # " do echo Namespace: \${ns}; ip netns exec \${ns} ip r;"
434 # "done > /root/\$(hostname -f)/dump_ip_r_ns.txt;"
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200435
Dennis Dmitriev21369672018-03-24 14:50:18 +0200436 # "for ns in \$(ip netns list);"
437 # " do echo Namespace: \${ns}; ip netns exec \${ns} netstat -anp;"
438 # "done > /root/\$(hostname -f)/dump_netstat_ns.txt;"
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200439
440 "/usr/bin/haproxy-status.sh > "
Dennis Dmitriev21369672018-03-24 14:50:18 +0200441 " /root/\$(hostname -f)/dump_haproxy.txt;"
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200442
443 # Archive the files
444 "cd /root/; tar --absolute-names --warning=no-file-changed "
Dennis Dmitriev21369672018-03-24 14:50:18 +0200445 " -czf \$(hostname -f).tar.gz ./\$(hostname -f)/;"
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200446 )
447
448 master_host = self.__config.salt.salt_master_host
449 with self.remote(host=master_host) as master:
450 # dump files
451 LOG.info("Archive artifacts on all nodes")
Dennis Dmitrievc83b3d42018-03-16 00:59:18 +0200452 master.check_call('salt "*" cmd.run "{0}"'.format(dump_commands),
Dennis Dmitrievbc229552018-07-24 13:54:59 +0300453 raise_on_err=False,
454 timeout=600)
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200455
456 # create target dir for archives
457 master.check_call("mkdir /root/dump/")
458
459 # get archived artifacts to the master node
460 for node in self.config_ssh:
461 LOG.info("Getting archived artifacts from the node {0}"
Dennis Dmitriev0bc485b2017-12-13 12:49:54 +0200462 .format(node['node_name']))
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200463 master.check_call("rsync -aruv {0}:/root/*.tar.gz "
464 "/root/dump/".format(node['node_name']),
Dennis Dmitriev8feb2522018-03-28 19:17:04 +0300465 raise_on_err=False,
466 timeout=120)
Dennis Dmitriev0bc485b2017-12-13 12:49:54 +0200467
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200468 destination_name = '/root/{0}_dump.tar.gz'.format(artifact_name)
469 # Archive the artifacts from all nodes
470 master.check_call(
471 'cd /root/dump/;'
472 'tar --absolute-names --warning=no-file-changed -czf '
473 ' {0} ./'.format(destination_name))
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300474
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200475 # Download the artifact to the host
Dennis Dmitriev2d643bc2017-12-04 12:23:47 +0200476 LOG.info("Downloading the artifact {0}".format(destination_name))
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200477 master.download(destination=destination_name, target=os.getcwd())
Dennis Dmitriev2d643bc2017-12-04 12:23:47 +0200478
479 def delayed_call(
480 self, cmd,
481 node_name=None, host=None, address_pool=None,
482 verbose=True, timeout=5,
483 delay_min=None, delay_max=None):
484 """Delayed call of the specified command in background
485
486 :param delay_min: minimum delay in minutes before run
487 the command
488 :param delay_max: maximum delay in minutes before run
489 the command
490 The command will be started at random time in the range
491 from delay_min to delay_max in minutes from 'now'
492 using the command 'at'.
493
494 'now' is rounded to integer by 'at' command, i.e.:
495 now(28 min 59 sec) == 28 min 00 sec.
496
497 So, if delay_min=1 , the command may start in range from
498 1 sec to 60 sec.
499
500 If delay_min and delay_max are None, then the command will
501 be executed in the background right now.
502 """
503 time_min = delay_min or delay_max
504 time_max = delay_max or delay_min
505
506 delay = None
507 if time_min is not None and time_max is not None:
508 delay = random.randint(time_min, time_max)
509
510 delay_str = ''
511 if delay:
512 delay_str = " + {0} min".format(delay)
513
514 delay_cmd = "cat << EOF | at now {0}\n{1}\nEOF".format(delay_str, cmd)
515
516 self.check_call(delay_cmd, node_name=node_name, host=host,
517 address_pool=address_pool, verbose=verbose,
518 timeout=timeout)
519
520 def get_target_node_names(self, target='gtw01.'):
521 """Get all node names which names starts with <target>"""
522 return [node_name for node_name
523 in self.node_names()
524 if node_name.startswith(target)]