Merge ~jocave/checkbox-support:snapd-api-rework into checkbox-support: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)
Reviewer Review Type Date Requested Status
Sylvain Pineau (community) Approve
Review via email: mp+365454@code.launchpad.net

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 :

just a question, see below. I'll test it later

review: Needs Information
Revision history for this message
Jonathan Cave (jocave) wrote :

Good spot, removed the debug cruft

Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

+1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/checkbox_support/scripts/snap_configuration.py b/checkbox_support/scripts/snap_configuration.py
2deleted file mode 100644
3index 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())
62diff --git a/checkbox_support/scripts/snap_connect.py b/checkbox_support/scripts/snap_connect.py
63index 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()
183diff --git a/checkbox_support/snap_utils/config.py b/checkbox_support/snap_utils/config.py
184index 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
251diff --git a/checkbox_support/snap_utils/snapd.py b/checkbox_support/snap_utils/snapd.py
252new file mode 100644
253index 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)
403diff --git a/setup.py b/setup.py
404index 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="

Subscribers

People subscribed via source and target branches