Merge lp:~canonical-platform-qa/snappy-ecosystem-tests/helpers_for_snapd_cli into lp:snappy-ecosystem-tests
- helpers_for_snapd_cli
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Heber Parrucci |
Approved revision: | 37 |
Merged at revision: | 23 |
Proposed branch: | lp:~canonical-platform-qa/snappy-ecosystem-tests/helpers_for_snapd_cli |
Merge into: | lp:snappy-ecosystem-tests |
Prerequisite: | lp:~canonical-platform-qa/snappy-ecosystem-tests/minor_tweaks |
Diff against target: |
359 lines (+314/-0) 6 files modified
README.rst (+6/-0) requirements.txt (+2/-0) snappy_ecosystem_tests/helpers/snapd/snapd.py (+137/-0) snappy_ecosystem_tests/tests/test_snapd.py (+42/-0) snappy_ecosystem_tests/utils/ssh.py (+92/-0) snappy_ecosystem_tests/utils/user.py (+35/-0) |
To merge this branch: | bzr merge lp:~canonical-platform-qa/snappy-ecosystem-tests/helpers_for_snapd_cli |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Heber Parrucci (community) | Approve | ||
Santiago Baldassin (community) | Approve | ||
platform-qa-bot | continuous-integration | Approve | |
I Ahmad (community) | Needs Fixing | ||
Review via email: mp+316459@code.launchpad.net |
Commit message
Basic helpers around snapd' command line interface
Description of the change
Basic helpers around snapd' command line interface
I Ahmad (iahmad) : | # |
- 8. By Omer Akram
-
merge with trunk
- 9. By Omer Akram
-
Assert when logged in
Omer Akram (om26er) wrote : | # |
Replied inline.
- 10. By Omer Akram
-
fix login command
I Ahmad (iahmad) wrote : | # |
I am fine as long as data is not parsed/filtered within helper method
On Tue, Feb 7, 2017 at 7:48 PM, Omer Akram <email address hidden> wrote:
> Replied inline.
>
> Diff comments:
>
> >
> > === modified file 'tests/
> > --- tests/utils/
> > +++ tests/utils/
> > @@ -17,3 +17,138 @@
> > # You should have received a copy of the GNU General Public License
> > # along with this program. If not, see <http://
> > #
> > +
> > +import getpass
> > +import os
> > +import subprocess
> > +import sys
> > +import shlex
> > +import shutil
> > +import tempfile
> > +
> > +import pexpect
> > +import yaml
> > +
> > +PATH_SNAP = '/usr/bin/snap'
> > +COMMAND_DOWNLOAD = 'download {snap} --channel=
> > +COMMAND_FIND = 'find {search_term}'
> > +COMMAND_INFO = 'info {snap}'
> > +COMMAND_INSTALL = 'install {snap} --channel=
> > +COMMAND_LOGIN = 'login {email}'
> > +COMMAND_LOGOUT = 'logout'
> > +COMMAND_REFRESH = 'refresh {snap} --channel=
> > +CHANNEL_STABLE = 'stable'
> > +PASSWORD_ROOT = 'changed'
> > +
> > +
> > +def is_root_user():
> > + """Return bool representing if the current user is root."""
> > + return os.getuid() == 0
> > +
> > +
> > +def run_snapd_
> > + """ Run the request snapd cli command.
> > +
> > + :param parameters: the snapd sub-command and their parameters.
> > + example: install core --beta
> > + :param return_output: Whether to return the stdout of the command.
> > + :param cwd: The current working directory for the command, defaults
> to
> > + pwd.
> > + :return: Optionally return the stdout of the command, depending if
> > + the value of `return_output` was True.
> > + """
> > + command = '{} {}'.format(
> > + if return_output:
> > + raw_output = subprocess.
> > + shlex.split(
> > + return raw_output.
> > + subprocess.
> > +
> > +
> > +def login(email, password):
> > + """Login to snapd.
> > +
> > + :param email: Ubuntu SSO account email address.
> > + :param password: Ubuntu SSO account password.
> > + """
> > + command = '{} {}'.format(
> > + if is_root_user():
> > + p = pexpect.
> > + else:
> > + p = pexpect.spawnu('{} {}'.format('sudo', command),
> logfile=sys.stdout)
> > + p.expect_exact(
> > + '[sudo] password for {}: '.format(
> > + p.sendline(
> > + p.expect_
> > + p.sendline(
> > + p.wait()
> > +
>
> I added an assert to ensure the login was successful.
>
> > +
> > +def logout():
> > + run_snapd_
> > +
> > +
> > +def download(snap, channel=
> > + """Download the requested snap.
> > +
> > + :param snap: name of the snap to download.
> > + :param channel:...
- 11. By Omer Akram
-
Simplify .
./snapd.py
./snapcraft.py
./__pycache__
./__pycache__/snapd. cpython- 35.pyc
./__init__.py method to remove client side filtering
Omer Akram (om26er) wrote : | # |
Ok, removed the filtering code now.
platform-qa-bot (platform-qa-bot) wrote : | # |
FAILED: Continuous integration, rev:11
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
- 12. By Omer Akram
-
Merge with trunk
platform-qa-bot (platform-qa-bot) wrote : | # |
FAILED: Continuous integration, rev:12
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
Omer Akram (om26er) wrote : | # |
I have made the suggested changes.
- 13. By Omer Akram
-
Fix pylint complaints
platform-qa-bot (platform-qa-bot) wrote : | # |
PASSED: Continuous integration, rev:13
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
- 14. By Omer Akram
-
add fixme comment
platform-qa-bot (platform-qa-bot) wrote : | # |
PASSED: Continuous integration, rev:14
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
- 15. By Omer Akram
-
more compact code. always return command output
platform-qa-bot (platform-qa-bot) wrote : | # |
PASSED: Continuous integration, rev:15
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
Heber Parrucci (heber013) wrote : | # |
Looks good in general. Couple of comments inline. Additionally:
* Merge from trunk
* Can you add a single test that checks if it is possible to login in snapd? in /tests folder
* Can you move snapd.py from utils to helpers?
- 16. By Omer Akram
-
revert
- 17. By Omer Akram
-
merge with trunk
- 18. By Omer Akram
-
bring back changes
- 19. By Omer Akram
-
read password from config, other changes
- 20. By Omer Akram
-
add simple test case for snapd
Omer Akram (om26er) wrote : | # |
Replied inline also added a few simple test cases.
Heber Parrucci (heber013) wrote : | # |
Looks good. One comment inline
Omer Akram (om26er) wrote : | # |
sure, let me fix that.
platform-qa-bot (platform-qa-bot) wrote : | # |
FAILED: Continuous integration, rev:20
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
- 21. By Omer Akram
-
Update snapd helpers to execute over ssh
platform-qa-bot (platform-qa-bot) wrote : | # |
FAILED: Continuous integration, rev:21
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
- 22. By Omer Akram
-
Merge with trunk
- 23. By Omer Akram
-
pylint happy
platform-qa-bot (platform-qa-bot) wrote : | # |
FAILED: Continuous integration, rev:23
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
Omer Akram (om26er) wrote : | # |
I updated this branch to execute all commands over ssh. With paramiko we automagically get a persistent ssh connection, so the performance should not be a problem. The remote machine should have `expect` installed. Both snapd and snapcraft commands should work just fine over ssh. For store api calls, we need a small wrapper that builds curl commands based on our requirements to be run over ssh.
with that we can have a single way to run tests on all targets (kvm, bare metal, all-snap(in future), lxd or a tablet.)
TODO: run_command_
platform-qa-bot (platform-qa-bot) wrote : | # |
FAILED: Continuous integration, rev:23
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
Heber Parrucci (heber013) wrote : | # |
Looks good. Just minor comments
- 24. By Omer Akram
-
Maintain persistent ssh object, fix review comments
- 25. By Omer Akram
-
rename and move ssh runner function to ssh.py
Omer Akram (om26er) wrote : | # |
Replied inline.
platform-qa-bot (platform-qa-bot) wrote : | # |
FAILED: Continuous integration, rev:25
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
- 26. By Omer Akram
-
More readable calls
platform-qa-bot (platform-qa-bot) wrote : | # |
FAILED: Continuous integration, rev:26
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
- 27. By Omer Akram
-
fix some words
platform-qa-bot (platform-qa-bot) wrote : | # |
FAILED: Continuous integration, rev:27
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
- 28. By Omer Akram
-
ignore chrome driver from requirements
platform-qa-bot (platform-qa-bot) wrote : | # |
PASSED: Continuous integration, rev:28
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
Santiago Baldassin (sbaldassin) wrote : | # |
Looks good but I think the ssh approach does not entirely solve the issue we discussed. See inline
Omer Akram (om26er) wrote : | # |
Added a comment.
Omer Akram (om26er) wrote : | # |
Adding slightly less relevant comment.
- 29. By Omer Akram
-
Add protection against direct initialization of SSHClient
platform-qa-bot (platform-qa-bot) wrote : | # |
FAILED: Continuous integration, rev:29
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
- 30. By Omer Akram
-
Merge with trunk
platform-qa-bot (platform-qa-bot) wrote : | # |
PASSED: Continuous integration, rev:30
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
Heber Parrucci (heber013) wrote : | # |
See comments inline.
- 31. By Omer Akram
-
Add optional overriding of hostname and username to run commands
Omer Akram (om26er) wrote : | # |
Replied inline, also extended run_command to optionally take hostname and username as suggested in your code.
platform-qa-bot (platform-qa-bot) wrote : | # |
PASSED: Continuous integration, rev:31
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
Santiago Baldassin (sbaldassin) wrote : | # |
Reply inline. Let's talk about this over the phone before we get into an endless discussion
Omer Akram (om26er) wrote : | # |
Replied and lets chat.
Heber Parrucci (heber013) wrote : | # |
Reply inline. Let's talk offline
Sergio Cazzolato (sergio-j-cazzolato) wrote : | # |
Comment inline
- 32. By Omer Akram
-
Update to a SSHManager approach to keep a pool of active connections
platform-qa-bot (platform-qa-bot) wrote : | # |
PASSED: Continuous integration, rev:32
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
Omer Akram (om26er) wrote : | # |
Replied inline.
- 33. By Omer Akram
-
Add doc-string
Omer Akram (om26er) wrote : | # |
Sergio, replied inline.
platform-qa-bot (platform-qa-bot) wrote : | # |
PASSED: Continuous integration, rev:33
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
- 34. By Omer Akram
-
More simpler and compact approach
platform-qa-bot (platform-qa-bot) wrote : | # |
PASSED: Continuous integration, rev:34
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
- 35. By Omer Akram
-
reuse some code
platform-qa-bot (platform-qa-bot) wrote : | # |
PASSED: Continuous integration, rev:35
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
Heber Parrucci (heber013) wrote : | # |
Looks much better. Just a question inline.
Additionally, can you update README with remote host credentials info? you can reuse User Credentials section in that file if you wish.
- 36. By Omer Akram
-
update README for ssh credentials
Omer Akram (om26er) wrote : | # |
Updated README, added inline comments.
platform-qa-bot (platform-qa-bot) wrote : | # |
PASSED: Continuous integration, rev:36
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
Santiago Baldassin (sbaldassin) wrote : | # |
Looks good. Minor comments inline
Heber Parrucci (heber013) wrote : | # |
Reply to Santiago's suggestion
Omer Akram (om26er) wrote : | # |
Replied.
Omer Akram (om26er) wrote : | # |
re.
- 37. By Omer Akram
-
Changes
platform-qa-bot (platform-qa-bot) wrote : | # |
PASSED: Continuous integration, rev:37
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
Santiago Baldassin (sbaldassin) wrote : | # |
Looks Good. Thanks
Heber Parrucci (heber013) wrote : | # |
Code LGTM. Let's land it.
Preview Diff
1 | === modified file 'README.rst' |
2 | --- README.rst 2017-02-20 22:56:17 +0000 |
3 | +++ README.rst 2017-02-22 19:08:43 +0000 |
4 | @@ -69,10 +69,16 @@ |
5 | [user] |
6 | user_email=^USER_NAME^ |
7 | user_password=^USER_PASSWORD^ |
8 | +hostname_remote=^SSH_HOSTNAME^ |
9 | +username_remote=^SSH_USERNAME^ |
10 | +port_remote=^SSH_PORT^ |
11 | |
12 | option 2 - Set the following environment variables: |
13 | user_email=^USER_NAME^ |
14 | user_password=^USER_PASSWORD^ |
15 | +hostname_remote=^SSH_HOSTNAME^ |
16 | +username_remote=^SSH_USERNAME^ |
17 | +port_remote=^SSH_PORT^ |
18 | |
19 | |
20 | Changing store: |
21 | |
22 | === modified file 'requirements.txt' |
23 | --- requirements.txt 2017-02-21 12:00:19 +0000 |
24 | +++ requirements.txt 2017-02-22 19:08:43 +0000 |
25 | @@ -16,3 +16,5 @@ |
26 | requests-toolbelt==0.6.0 |
27 | #chromedriver_installer |
28 | pylxd |
29 | +pyyaml |
30 | +paramiko |
31 | |
32 | === modified file 'snappy_ecosystem_tests/helpers/snapd/snapd.py' |
33 | --- snappy_ecosystem_tests/helpers/snapd/snapd.py 2017-02-09 20:09:40 +0000 |
34 | +++ snappy_ecosystem_tests/helpers/snapd/snapd.py 2017-02-22 19:08:43 +0000 |
35 | @@ -17,3 +17,140 @@ |
36 | # You should have received a copy of the GNU General Public License |
37 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
38 | # |
39 | + |
40 | +"""Helpers around snapd command-line interface.""" |
41 | + |
42 | +import json |
43 | +import logging |
44 | + |
45 | +import yaml |
46 | + |
47 | +from snappy_ecosystem_tests.utils import ssh |
48 | + |
49 | +PATH_SNAP = '/usr/bin/snap' |
50 | +COMMAND_DOWNLOAD = 'download {snap} --channel={channel}' |
51 | +COMMAND_FIND = 'find {search_term}' |
52 | +COMMAND_INFO = 'info {snap}' |
53 | +COMMAND_INSTALL = 'install {snap} --channel={channel}' |
54 | +COMMAND_LIST = 'list' |
55 | +COMMAND_LOGIN = 'login {email}' |
56 | +COMMAND_LOGOUT = 'logout' |
57 | +COMMAND_REFRESH = 'refresh {snap} --channel={channel}' |
58 | +COMMAND_REMOVE = 'remove {snap}' |
59 | +CHANNEL_STABLE = 'stable' |
60 | +COMMANDS_LOGIN = """\ |
61 | +/usr/bin/expect \ |
62 | +-c 'spawn snap login {email}' \ |
63 | +-c 'expect \"Password*\"' \ |
64 | +-c 'send {password}\\r' \ |
65 | +-c 'interact'\ |
66 | +""" |
67 | +LOGGER = logging.getLogger(__name__) |
68 | + |
69 | + |
70 | +def run_snapd_command_ssh(parameters, cwd=''): |
71 | + """Run snapd command over ssh. |
72 | + |
73 | + :param parameters: a string containing parameters for the `snap` command. |
74 | + :param cwd: the current working directory on the remote where the command |
75 | + should run. |
76 | + :returns: stdout of the command |
77 | + """ |
78 | + if cwd: |
79 | + return ssh.run_command( |
80 | + 'cd {}; {} {}'.format(cwd, PATH_SNAP, parameters)) |
81 | + return ssh.run_command('{} {}'.format(PATH_SNAP, parameters)) |
82 | + |
83 | + |
84 | +def login(email, password): |
85 | + """Login to snapd. |
86 | + |
87 | + :param email: Ubuntu SSO account email address. |
88 | + :param password: Ubuntu SSO account password. |
89 | + """ |
90 | + ssh.run_command(COMMANDS_LOGIN.format(email=email, password=password)) |
91 | + return is_logged_in(email) |
92 | + |
93 | + |
94 | +def is_logged_in(email): |
95 | + """Return bool representing if the user is logged into snapd.""" |
96 | + try: |
97 | + return json.loads(ssh.run_command('cat ~/.snap/auth.json')).get( |
98 | + 'email') == email |
99 | + except ValueError: |
100 | + return False |
101 | + |
102 | + |
103 | +def logout(email): |
104 | + """Logout snapd current user.""" |
105 | + if is_logged_in(email): |
106 | + run_snapd_command_ssh(COMMAND_LOGOUT) |
107 | + |
108 | + |
109 | +def download(snap, channel=CHANNEL_STABLE): |
110 | + """Download the requested snap. |
111 | + |
112 | + :param snap: name of the snap to download. |
113 | + :param channel: name of the release channel to download from. |
114 | + """ |
115 | + command = COMMAND_DOWNLOAD.format(snap=snap, channel=channel) |
116 | + run_snapd_command_ssh(command, cwd=ssh.run_command('mktemp -d')) |
117 | + |
118 | + |
119 | +def install(snap, channel=CHANNEL_STABLE): |
120 | + """Install the requested snap.""" |
121 | + run_snapd_command_ssh(COMMAND_INSTALL.format(snap=snap, channel=channel)) |
122 | + |
123 | + |
124 | +def _is_installed(snap): |
125 | + """Return bool representing whether a snap is installed.""" |
126 | + for installed_snap in _parse_output(run_snapd_command_ssh(COMMAND_LIST)): |
127 | + if installed_snap['name'] == snap: |
128 | + return True |
129 | + return False |
130 | + |
131 | + |
132 | +def remove(snap): |
133 | + """Remove a snap, if its already installed.""" |
134 | + if _is_installed(snap): |
135 | + run_snapd_command_ssh(COMMAND_REMOVE.format(snap=snap)) |
136 | + |
137 | + |
138 | +def info(snap, verbose=False): |
139 | + """Query the Ubuntu store of the information about a snap. |
140 | + |
141 | + :param snap: Name of the snap for which the info is required. |
142 | + :param verbose: Whether to information should be detailed. |
143 | + :return: Return a dictionary containing information about the snap. |
144 | + """ |
145 | + command = COMMAND_INFO.format(snap=snap) |
146 | + if verbose: |
147 | + command = ' '.join([command, '--verbose']) |
148 | + return yaml.load("""{}""".format(run_snapd_command_ssh(command))) |
149 | + |
150 | + |
151 | +def refresh(snap, channel=CHANNEL_STABLE): |
152 | + """Refresh the requested snap.""" |
153 | + run_snapd_command_ssh(COMMAND_REFRESH.format(snap=snap, channel=channel)) |
154 | + |
155 | + |
156 | +def _parse_output(raw_output): |
157 | + """Pretty parse the output from snapd commands like `find` and `list`. |
158 | + |
159 | + :param raw_output: The raw output returned from the command |
160 | + :return: A list of dictionaries containing sorted results from the output. |
161 | + """ |
162 | + split_output = raw_output.split('\n') |
163 | + headers = [header.lower() for header in split_output.pop(0).split()] |
164 | + return [dict(zip(headers, line.split())) for line in split_output] |
165 | + |
166 | + |
167 | +def find(keyword): |
168 | + """Find snaps based on the provided filters |
169 | + |
170 | + :param keyword: Keyword to use for the query. |
171 | + :return: Return a list of dictionaries containing information about |
172 | + snaps matching the `keyword`. |
173 | + """ |
174 | + return _parse_output( |
175 | + run_snapd_command_ssh(COMMAND_FIND.format(search_term=keyword))) |
176 | |
177 | === added file 'snappy_ecosystem_tests/tests/test_snapd.py' |
178 | --- snappy_ecosystem_tests/tests/test_snapd.py 1970-01-01 00:00:00 +0000 |
179 | +++ snappy_ecosystem_tests/tests/test_snapd.py 2017-02-22 19:08:43 +0000 |
180 | @@ -0,0 +1,42 @@ |
181 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
182 | + |
183 | +# |
184 | +# Snappy Ecosystem Tests |
185 | +# Copyright (C) 2017 Canonical |
186 | +# |
187 | +# This program is free software: you can redistribute it and/or modify |
188 | +# it under the terms of the GNU General Public License as published by |
189 | +# the Free Software Foundation, either version 3 of the License, or |
190 | +# (at your option) any later version. |
191 | +# |
192 | +# This program is distributed in the hope that it will be useful, |
193 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
194 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
195 | +# GNU General Public License for more details. |
196 | +# |
197 | +# You should have received a copy of the GNU General Public License |
198 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
199 | +# |
200 | + |
201 | +"""Snapd testcases.""" |
202 | + |
203 | +import testtools |
204 | + |
205 | +from snappy_ecosystem_tests.helpers.snapd import snapd |
206 | +from snappy_ecosystem_tests.utils.storeconfig import get_store_credentials |
207 | + |
208 | + |
209 | +class SnapdTestCase(testtools.TestCase): |
210 | + """Tests for snapd.""" |
211 | + def setUp(self): |
212 | + super().setUp() |
213 | + |
214 | + def test_info(self): |
215 | + """Ensure the publisher of core snap in canonical.""" |
216 | + self.assertTrue('canonical', snapd.info('core')['publisher']) |
217 | + |
218 | + def test_login(self): |
219 | + """Login the snapd user.""" |
220 | + email, password = get_store_credentials() |
221 | + self.assertTrue(snapd.login(email, password)) |
222 | + self.addCleanup(snapd.logout, email) |
223 | |
224 | === added file 'snappy_ecosystem_tests/utils/ssh.py' |
225 | --- snappy_ecosystem_tests/utils/ssh.py 1970-01-01 00:00:00 +0000 |
226 | +++ snappy_ecosystem_tests/utils/ssh.py 2017-02-22 19:08:43 +0000 |
227 | @@ -0,0 +1,92 @@ |
228 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
229 | + |
230 | +# |
231 | +# Snappy Ecosystem Tests |
232 | +# Copyright (C) 2017 Canonical |
233 | +# |
234 | +# This program is free software: you can redistribute it and/or modify |
235 | +# it under the terms of the GNU General Public License as published by |
236 | +# the Free Software Foundation, either version 3 of the License, or |
237 | +# (at your option) any later version. |
238 | +# |
239 | +# This program is distributed in the hope that it will be useful, |
240 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
241 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
242 | +# GNU General Public License for more details. |
243 | +# |
244 | +# You should have received a copy of the GNU General Public License |
245 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
246 | +# |
247 | + |
248 | +"""Module to connect to ssh client for running tests.""" |
249 | + |
250 | +import paramiko |
251 | + |
252 | +from snappy_ecosystem_tests.utils.user import get_remote_host_credentials |
253 | + |
254 | + |
255 | +class SSHManager: |
256 | + """Manager class to keep a pool of ssh connections.""" |
257 | + # Mangle the pool so that it cannot be easily touched from outside. |
258 | + __connection_pool = [] |
259 | + |
260 | + def __init__(self): |
261 | + raise NotImplementedError( |
262 | + 'Class cannot be instantiated, use get_instance() to get a ' |
263 | + 'SSHClient instance.') |
264 | + |
265 | + @staticmethod |
266 | + def get_instance(hostname, username, port=22, **kwargs): |
267 | + """Return the instance of SSHClient from a pool of active connections, |
268 | + otherwise create a new connection and return.""" |
269 | + port = int(port) |
270 | + client = SSHManager.get_client(hostname, username, port) |
271 | + if client: |
272 | + if not client.get_transport().is_active(): |
273 | + client.connect( |
274 | + hostname, username=username, port=port, **kwargs) |
275 | + else: |
276 | + client = paramiko.SSHClient() |
277 | + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) |
278 | + client.connect(hostname, username=username, port=port, **kwargs) |
279 | + SSHManager.__connection_pool.append(client) |
280 | + return client |
281 | + |
282 | + @staticmethod |
283 | + def sync(): |
284 | + """Update the connection pool to only keep alive connections.""" |
285 | + pool_copy = SSHManager.__connection_pool.copy() |
286 | + for client in SSHManager.__connection_pool: |
287 | + if not client.get_transport().is_alive(): |
288 | + pool_copy.remove(client) |
289 | + SSHManager.__connection_pool = pool_copy |
290 | + |
291 | + @staticmethod |
292 | + def get_client(hostname, username, port): |
293 | + """Return the matching client from the pool based on provided |
294 | + constraints.""" |
295 | + connections = [ |
296 | + client for client in SSHManager.__connection_pool if |
297 | + client.get_transport().getpeername() == (hostname, port) and |
298 | + username == client.get_transport().get_username() |
299 | + ] |
300 | + return connections[0] if connections else None |
301 | + |
302 | + |
303 | +def run_command(command, hostname=None, username=None, port=None): |
304 | + """Run the given command on remote machine over ssh. |
305 | + |
306 | + :param command: a string of the command to run. |
307 | + :param hostname: The host to run command on. |
308 | + :param username: Name of the user on the remote host to login to. |
309 | + :param port: SSH port number. |
310 | + :raises ValueError: if command exits with non-zero status. |
311 | + :return: the stdout of the command. |
312 | + """ |
313 | + _hostname, _username, _port = get_remote_host_credentials() |
314 | + ssh = SSHManager.get_instance( |
315 | + hostname or _hostname, username or _username, port or _port) |
316 | + _, stdout, stderr = ssh.exec_command(command) |
317 | + if stdout.channel.recv_exit_status() != 0: |
318 | + raise ValueError(stderr.read().decode()) |
319 | + return stdout.read().decode() |
320 | |
321 | === added file 'snappy_ecosystem_tests/utils/user.py' |
322 | --- snappy_ecosystem_tests/utils/user.py 1970-01-01 00:00:00 +0000 |
323 | +++ snappy_ecosystem_tests/utils/user.py 2017-02-22 19:08:43 +0000 |
324 | @@ -0,0 +1,35 @@ |
325 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
326 | + |
327 | +# |
328 | +# Snappy Ecosystem Tests |
329 | +# Copyright (C) 2017 Canonical |
330 | +# |
331 | +# This program is free software: you can redistribute it and/or modify |
332 | +# it under the terms of the GNU General Public License as published by |
333 | +# the Free Software Foundation, either version 3 of the License, or |
334 | +# (at your option) any later version. |
335 | +# |
336 | +# This program is distributed in the hope that it will be useful, |
337 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
338 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
339 | +# GNU General Public License for more details. |
340 | +# |
341 | +# You should have received a copy of the GNU General Public License |
342 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
343 | +# |
344 | + |
345 | +"""Get host user data.""" |
346 | + |
347 | +import os |
348 | + |
349 | +from snappy_ecosystem_tests.commons.config import USER_CONFIG_STACK |
350 | + |
351 | + |
352 | +def get_remote_host_credentials(): |
353 | + """Return credentials for remote machine, to run commands on.""" |
354 | + return (USER_CONFIG_STACK.get('user', 'hostname_remote', |
355 | + default=os.environ.get('hostname_remote')), |
356 | + USER_CONFIG_STACK.get('user', 'username_remote', |
357 | + default=os.environ.get('username_remote')), |
358 | + USER_CONFIG_STACK.get('user', 'port_remote', |
359 | + default=os.environ.get('port_remote'))) |
Please see my inline comments