Merge ~jocave/checkbox-support:snapd-api-rework into checkbox-support:master
- Git
- lp:~jocave/checkbox-support
- snapd-api-rework
- Merge into master
Proposed by
Jonathan Cave
Status: | Merged |
---|---|
Approved by: | Jonathan Cave |
Approved revision: | 2987aa48753c376ca78ff24a41f3deb2a9d42403 |
Merged at revision: | 530bf5ffedc2a15ac4f5920f9d22b9e637811560 |
Proposed branch: | ~jocave/checkbox-support:snapd-api-rework |
Merge into: | checkbox-support:master |
Diff against target: |
415 lines (+164/-168) 5 files modified
checkbox_support/scripts/snap_connect.py (+16/-63) checkbox_support/snap_utils/config.py (+2/-48) checkbox_support/snap_utils/snapd.py (+146/-0) dev/null (+0/-55) setup.py (+0/-2) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Sylvain Pineau (community) | Approve | ||
Review via email: mp+365454@code.launchpad.net |
Commit message
Description of the change
As part of de-duplication of snapd REST API code, this MR moves creates Snapd class that is easily importable in to other scripts. Other snapd code in this package are reworked on top of this.
Follow up MRs for providers demonstrate usage and were used to test the changes. Also tested that configure scripts in checkbox snaps still work as previously.
To post a comment you must log in.
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote : | # |
review:
Needs Information
Revision history for this message
Jonathan Cave (jocave) wrote : | # |
Good spot, removed the debug cruft
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/checkbox_support/scripts/snap_configuration.py b/checkbox_support/scripts/snap_configuration.py |
2 | deleted file mode 100644 |
3 | index 4167913..0000000 |
4 | --- a/checkbox_support/scripts/snap_configuration.py |
5 | +++ /dev/null |
6 | @@ -1,55 +0,0 @@ |
7 | -#!/usr/bin/env python3 |
8 | -# Copyright 2017 Canonical Ltd. |
9 | -# All rights reserved. |
10 | -# |
11 | -# Written by: |
12 | -# Authors: Jonathan Cave <jonathan.cave@canonical.com> |
13 | - |
14 | -import argparse |
15 | -import sys |
16 | - |
17 | -from checkbox_support.snap_utils.config import (get_configuration, |
18 | - set_configuration) |
19 | - |
20 | - |
21 | -def main(): |
22 | - desc = 'Query snapd for configuration details of an installed snap' |
23 | - parser = argparse.ArgumentParser(description=desc) |
24 | - |
25 | - subparsers = parser.add_subparsers(dest='action') |
26 | - subparsers.required = True |
27 | - |
28 | - parser_get = subparsers.add_parser('get', help='Get configuration item') |
29 | - parser_get.add_argument('snap', type=str, help='snap name') |
30 | - parser_get.add_argument('key', type=str, help='configuration item key') |
31 | - |
32 | - parser_set = subparsers.add_parser('set', help='Set configuration item') |
33 | - parser_set.add_argument('snap', type=str, help='snap name') |
34 | - parser_set.add_argument('key', type=str, help='configuration item key') |
35 | - parser_set.add_argument('value', type=str, help='new value') |
36 | - |
37 | - parser_test = subparsers.add_parser( |
38 | - 'test', help='Test configuration item is set to the expected value') |
39 | - parser_test.add_argument('snap', type=str, help='snap name') |
40 | - parser_test.add_argument('key', type=str, help='configuration item key') |
41 | - parser_test.add_argument('expected_value', type=str, |
42 | - help='the expected value') |
43 | - |
44 | - args = parser.parse_args() |
45 | - if args.action == 'get': |
46 | - value = get_configuration(args.snap, args.key) |
47 | - print(value) |
48 | - if args.action == 'set': |
49 | - result = set_configuration(args.snap, args.key, args.value) |
50 | - print(result) |
51 | - if args.action == 'test': |
52 | - print('Expected value: {}'.format(args.expected_value)) |
53 | - value = get_configuration(args.snap, args.key) |
54 | - print('Value found on the {} snap: {}'.format(args.snap, value)) |
55 | - if args.expected_value != value: |
56 | - return 1 |
57 | - return 0 |
58 | - |
59 | - |
60 | -if __name__ == '__main__': |
61 | - sys.exit(main()) |
62 | diff --git a/checkbox_support/scripts/snap_connect.py b/checkbox_support/scripts/snap_connect.py |
63 | index 7e8fac0..72dc379 100644 |
64 | --- a/checkbox_support/scripts/snap_connect.py |
65 | +++ b/checkbox_support/scripts/snap_connect.py |
66 | @@ -29,22 +29,29 @@ is equivalent to: |
67 | """ |
68 | |
69 | import argparse |
70 | -import json |
71 | import logging |
72 | -import requests_unixsocket |
73 | import os |
74 | -import sys |
75 | -import time |
76 | from collections import namedtuple |
77 | |
78 | +from checkbox_support.snap_utils.snapd import Snapd |
79 | +import requests |
80 | |
81 | Connection = namedtuple( |
82 | 'Connection', |
83 | ['target_snap', 'target_slot', 'plug_snap', 'plug_plug']) |
84 | |
85 | |
86 | -class BadRequest(Exception): |
87 | - pass |
88 | +def get_connections(): |
89 | + data = Snapd().interfaces() |
90 | + connections = [] |
91 | + if 'plugs' in data: |
92 | + for plug in data['plugs']: |
93 | + if 'connections' in plug: |
94 | + for con in plug['connections']: |
95 | + connections.append(Connection( |
96 | + con['snap'], con['slot'], |
97 | + plug['snap'], plug['plug'])) |
98 | + return connections |
99 | |
100 | |
101 | def main(): |
102 | @@ -53,77 +60,23 @@ def main(): |
103 | 'connections', nargs='+', default=[], |
104 | metavar='plug:target_snap:target_slot') |
105 | args = parser.parse_args() |
106 | - api = SnapdAPI() |
107 | |
108 | for conn in [spec.split(':') for spec in args.connections]: |
109 | if len(conn) != 3: |
110 | raise SystemExit("Bad connection description") |
111 | assert os.environ['SNAP_NAME'] |
112 | snap = os.environ['SNAP_NAME'] |
113 | - existing_connections = api.get_connections() |
114 | + existing_connections = get_connections() |
115 | new_connection = Connection( |
116 | target_snap=conn[1], target_slot=conn[2], |
117 | plug_snap=snap, plug_plug=conn[0]) |
118 | if new_connection not in existing_connections: |
119 | try: |
120 | - api.connect(new_connection) |
121 | - except BadRequest as exc: |
122 | + Snapd().connect(*new_connection) |
123 | + except requests.HTTPError as exc: |
124 | logging.warning("Failed to connect %s to %s. %s" % ( |
125 | conn[0], conn[1], exc)) |
126 | |
127 | |
128 | -class SnapdAPI(): |
129 | - """Based on https://github.com/snapcore/snapd/wiki/REST-API""" |
130 | - SNAPD_BASE_URL = 'http+unix://%2Frun%2Fsnapd.socket' |
131 | - |
132 | - def __init__(self): |
133 | - self.session = requests_unixsocket.Session() |
134 | - |
135 | - def get_connections(self): |
136 | - data = self._get('/v2/interfaces') |
137 | - connections = [] |
138 | - if 'plugs' in data: |
139 | - for plug in data['plugs']: |
140 | - if 'connections' in plug: |
141 | - for con in plug['connections']: |
142 | - connections.append(Connection( |
143 | - con['snap'], con['slot'], |
144 | - plug['snap'], plug['plug'])) |
145 | - return connections |
146 | - |
147 | - def connect(self, con): |
148 | - json_data = json.dumps({ |
149 | - 'action': 'connect', |
150 | - 'slots': [{'snap': con.target_snap, 'slot': con.target_slot}], |
151 | - 'plugs': [{'snap': con.plug_snap, 'plug': con.plug_plug}] |
152 | - }) |
153 | - res = self._post('/v2/interfaces', json_data) |
154 | - ready = False |
155 | - while not ready: |
156 | - # busy wait until snapd reports connection job as finised |
157 | - time.sleep(0.5) |
158 | - con_res = self._get('/v2/changes/{}'.format(res['change'])) |
159 | - ready = con_res['ready'] |
160 | - |
161 | - def _get(self, path): |
162 | - res = self.session.get(self.SNAPD_BASE_URL + path) |
163 | - if not res.ok: |
164 | - logging.error("Got error %i attempting to access %s", |
165 | - res.status_code, path) |
166 | - sys.exit(1) |
167 | - return res.json()['result'] |
168 | - |
169 | - def _post(self, path, data=None): |
170 | - res = self.session.post(self.SNAPD_BASE_URL + path, data=data) |
171 | - if not res.ok: |
172 | - full_res = json.loads(res.text) |
173 | - if res.status_code == 400: |
174 | - raise BadRequest(full_res['result']['message']) |
175 | - logging.error("Got error %i attempting to access %s", |
176 | - res.status_code, path) |
177 | - sys.exit(1) |
178 | - return res.json() |
179 | - |
180 | - |
181 | if __name__ == '__main__': |
182 | main() |
183 | diff --git a/checkbox_support/snap_utils/config.py b/checkbox_support/snap_utils/config.py |
184 | index c33bafc..98083be 100644 |
185 | --- a/checkbox_support/snap_utils/config.py |
186 | +++ b/checkbox_support/snap_utils/config.py |
187 | @@ -12,52 +12,6 @@ import re |
188 | import subprocess |
189 | import sys |
190 | |
191 | -import requests |
192 | -import requests_unixsocket |
193 | - |
194 | -SNAPD_BASE_URL = 'http+unix://%2Frun%2Fsnapd.socket' |
195 | - |
196 | - |
197 | -class SnapdQuery(): |
198 | - |
199 | - def __init__(self): |
200 | - self._session = requests_unixsocket.Session() |
201 | - |
202 | - def get(self, path, params=None): |
203 | - r = self._session.get(SNAPD_BASE_URL + path, params=params) |
204 | - if r.status_code != requests.codes.ok: |
205 | - raise SystemExit('Got error {} attempting to access {}\n'.format( |
206 | - r.status_code, path)) |
207 | - return r |
208 | - |
209 | - def post(self, path, data=None): |
210 | - res = self._session.post(SNAPD_BASE_URL + path, data=data) |
211 | - if not res.ok: |
212 | - raise SystemExit('Got error {} attempting to post to {}\n'.format( |
213 | - res.status_code, path)) |
214 | - return res.json() |
215 | - |
216 | - def put(self, path, data=None): |
217 | - res = self._session.put(SNAPD_BASE_URL + path, data=data) |
218 | - if not res.ok: |
219 | - raise SystemExit('Got error {} attempting to put to {}\n'.format( |
220 | - res.status_code, path)) |
221 | - return res.json() |
222 | - |
223 | - |
224 | -def get_configuration(snap, key): |
225 | - path = '/v2/snaps/{}/conf'.format(snap) |
226 | - params = 'keys={}'.format(key) |
227 | - query = SnapdQuery() |
228 | - return query.get(path, params).json()['result'][key] |
229 | - |
230 | - |
231 | -def set_configuration(snap, key, value): |
232 | - path = '/v2/snaps/{}/conf'.format(snap) |
233 | - json_data = json.dumps({key: value}) |
234 | - query = SnapdQuery() |
235 | - return query.put(path, json_data)['status'] |
236 | - |
237 | |
238 | def get_snapctl_config(keys): |
239 | """Query snapctl for given keys.""" |
240 | @@ -95,8 +49,8 @@ def get_configuration_set(): |
241 | k = k.replace('_', '-').lower() |
242 | config_set[k] = v |
243 | except FileNotFoundError: |
244 | - # silently ignore missing config_vars |
245 | - pass |
246 | + # silently ignore missing config_vars |
247 | + pass |
248 | return config_set |
249 | |
250 | |
251 | diff --git a/checkbox_support/snap_utils/snapd.py b/checkbox_support/snap_utils/snapd.py |
252 | new file mode 100644 |
253 | index 0000000..6987a29 |
254 | --- /dev/null |
255 | +++ b/checkbox_support/snap_utils/snapd.py |
256 | @@ -0,0 +1,146 @@ |
257 | +# Copyright 2019 Canonical Ltd. |
258 | +# All rights reserved. |
259 | +# |
260 | +# Written by: |
261 | +# Jonathan Cave <jonathan.cave@canonical.com> |
262 | + |
263 | +import json |
264 | +import time |
265 | + |
266 | +import requests_unixsocket |
267 | + |
268 | + |
269 | +class AsyncException(Exception): |
270 | + |
271 | + def __init__(self, message): |
272 | + self.message = message |
273 | + |
274 | + |
275 | +class Snapd(): |
276 | + |
277 | + _url = 'http+unix://%2Frun%2Fsnapd.socket' |
278 | + |
279 | + _snaps = '/v2/snaps' |
280 | + _find = '/v2/find' |
281 | + _changes = '/v2/changes' |
282 | + _system_info = '/v2/system-info' |
283 | + _interfaces = '/v2/interfaces' |
284 | + _assertions = '/v2/assertions' |
285 | + |
286 | + def __init__(self): |
287 | + self._session = requests_unixsocket.Session() |
288 | + |
289 | + def _get(self, path, params=None, decode=True): |
290 | + r = self._session.get(self._url + path, params=params) |
291 | + r.raise_for_status() |
292 | + if decode: |
293 | + return r.json() |
294 | + return r |
295 | + |
296 | + def _post(self, path, data=None, decode=True): |
297 | + r = self._session.post(self._url + path, data=data) |
298 | + r.raise_for_status() |
299 | + if decode: |
300 | + return r.json() |
301 | + return r |
302 | + |
303 | + def _put(self, path, data=None, decode=True): |
304 | + r = self._session.put(self._url + path, data=data) |
305 | + r.raise_for_status() |
306 | + if decode: |
307 | + return r.json() |
308 | + return r |
309 | + |
310 | + def _poll_change(self, change_id, timeout=30): |
311 | + for _ in range(30): |
312 | + status = self.change(change_id) |
313 | + if status == 'Done': |
314 | + return True |
315 | + time.sleep(1) |
316 | + raise AsyncException(status) |
317 | + |
318 | + def list(self, snap=None): |
319 | + path = self._snaps |
320 | + if snap is not None: |
321 | + path += '/' + snap |
322 | + return self._get(path)['result'] |
323 | + |
324 | + def install(self, snap, channel='stable', revision=None): |
325 | + path = self._snaps + '/' + snap |
326 | + data = {'action': 'install', 'channel': channel} |
327 | + if revision is not None: |
328 | + data['revision'] = revision |
329 | + r = self._post(path, json.dumps(data)) |
330 | + if r['type'] == 'async' and r['status'] == 'Accepted': |
331 | + self._poll_change(r['change']) |
332 | + |
333 | + def remove(self, snap, revision=None): |
334 | + path = self._snaps + '/' + snap |
335 | + data = {'action': 'remove'} |
336 | + if revision is not None: |
337 | + data['revision'] = revision |
338 | + r = self._post(path, json.dumps(data)) |
339 | + if r['type'] == 'async' and r['status'] == 'Accepted': |
340 | + self._poll_change(r['change']) |
341 | + |
342 | + def find(self, search, exact=False): |
343 | + if exact: |
344 | + p = 'name={}'.format(search) |
345 | + else: |
346 | + p = 'q={}'.format(search) |
347 | + return self._get(self._find, params=p)['result'] |
348 | + |
349 | + def info(self, snap): |
350 | + return self.find(snap, exact=True)[0] |
351 | + |
352 | + def refresh(self, snap, channel='stable', revision=None): |
353 | + path = self._snaps + '/' + snap |
354 | + data = {'action': 'refresh', 'channel': channel} |
355 | + if revision is not None: |
356 | + data['revision'] = revision |
357 | + r = self._post(path, json.dumps(data)) |
358 | + if r['type'] == 'async' and r['status'] == 'Accepted': |
359 | + self._poll_change(r['change']) |
360 | + |
361 | + def change(self, change_id): |
362 | + path = self._changes + '/' + change_id |
363 | + r = self._get(path) |
364 | + return r['result']['status'] |
365 | + |
366 | + def revert(self, snap, channel='stable', revision=None): |
367 | + path = self._snaps + '/' + snap |
368 | + data = {'action': 'revert', 'channel': channel} |
369 | + if revision is not None: |
370 | + data['revision'] = revision |
371 | + r = self._post(path, json.dumps(data)) |
372 | + if r['type'] == 'async' and r['status'] == 'Accepted': |
373 | + self._poll_change(r['change']) |
374 | + |
375 | + def get_configuration(self, snap, key): |
376 | + path = self._snaps + '/' + snap + '/conf' |
377 | + p = 'keys={}'.format(key) |
378 | + return self._get(path, params=p)['result'][key] |
379 | + |
380 | + def set_configuration(self, snap, key, value): |
381 | + path = self._snaps + '/' + snap + '/conf' |
382 | + data = {key: value} |
383 | + r = self._post(path, json.dumps(data)) |
384 | + if r['type'] == 'async' and r['status'] == 'Accepted': |
385 | + self._poll_change(r['change']) |
386 | + |
387 | + def interfaces(self): |
388 | + return self._get(self._interfaces)['result'] |
389 | + |
390 | + def connect(self, slot_snap, slot_slot, plug_snap, plug_plug): |
391 | + data = { |
392 | + 'action': 'connect', |
393 | + 'slots': [{'snap': slot_snap, 'slot': slot_slot}], |
394 | + 'plugs': [{'snap': plug_snap, 'plug': plug_plug}] |
395 | + } |
396 | + r = self._post(self._interfaces, json.dumps(data)) |
397 | + if r['type'] == 'async' and r['status'] == 'Accepted': |
398 | + self._poll_change(r['change']) |
399 | + |
400 | + def get_assertions(self, assertion_type): |
401 | + path = self._assertions + '/' + assertion_type |
402 | + return self._get(path, decode=False) |
403 | diff --git a/setup.py b/setup.py |
404 | index dd2ed8c..3c3ac85 100755 |
405 | --- a/setup.py |
406 | +++ b/setup.py |
407 | @@ -82,8 +82,6 @@ setup( |
408 | "checkbox_support.scripts.fwts_test:main"), |
409 | ("checkbox-support-usb_read_write=" |
410 | "checkbox_support.scripts.usb_read_write:run_read_write_test"), |
411 | - ("checkbox-support-snap_configuration=" |
412 | - "checkbox_support.scripts.snap_configuration:main"), |
413 | ("checkbox-support-nmea_test=" |
414 | "checkbox_support.scripts.nmea_test:main"), |
415 | ("checkbox-support-snap_connect=" |
just a question, see below. I'll test it later