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
diff --git a/checkbox_support/scripts/snap_configuration.py b/checkbox_support/scripts/snap_configuration.py
0deleted file mode 1006440deleted file mode 100644
index 4167913..0000000
--- a/checkbox_support/scripts/snap_configuration.py
+++ /dev/null
@@ -1,55 +0,0 @@
1#!/usr/bin/env python3
2# Copyright 2017 Canonical Ltd.
3# All rights reserved.
4#
5# Written by:
6# Authors: Jonathan Cave <jonathan.cave@canonical.com>
7
8import argparse
9import sys
10
11from checkbox_support.snap_utils.config import (get_configuration,
12 set_configuration)
13
14
15def main():
16 desc = 'Query snapd for configuration details of an installed snap'
17 parser = argparse.ArgumentParser(description=desc)
18
19 subparsers = parser.add_subparsers(dest='action')
20 subparsers.required = True
21
22 parser_get = subparsers.add_parser('get', help='Get configuration item')
23 parser_get.add_argument('snap', type=str, help='snap name')
24 parser_get.add_argument('key', type=str, help='configuration item key')
25
26 parser_set = subparsers.add_parser('set', help='Set configuration item')
27 parser_set.add_argument('snap', type=str, help='snap name')
28 parser_set.add_argument('key', type=str, help='configuration item key')
29 parser_set.add_argument('value', type=str, help='new value')
30
31 parser_test = subparsers.add_parser(
32 'test', help='Test configuration item is set to the expected value')
33 parser_test.add_argument('snap', type=str, help='snap name')
34 parser_test.add_argument('key', type=str, help='configuration item key')
35 parser_test.add_argument('expected_value', type=str,
36 help='the expected value')
37
38 args = parser.parse_args()
39 if args.action == 'get':
40 value = get_configuration(args.snap, args.key)
41 print(value)
42 if args.action == 'set':
43 result = set_configuration(args.snap, args.key, args.value)
44 print(result)
45 if args.action == 'test':
46 print('Expected value: {}'.format(args.expected_value))
47 value = get_configuration(args.snap, args.key)
48 print('Value found on the {} snap: {}'.format(args.snap, value))
49 if args.expected_value != value:
50 return 1
51 return 0
52
53
54if __name__ == '__main__':
55 sys.exit(main())
diff --git a/checkbox_support/scripts/snap_connect.py b/checkbox_support/scripts/snap_connect.py
index 7e8fac0..72dc379 100644
--- a/checkbox_support/scripts/snap_connect.py
+++ b/checkbox_support/scripts/snap_connect.py
@@ -29,22 +29,29 @@ is equivalent to:
29"""29"""
3030
31import argparse31import argparse
32import json
33import logging32import logging
34import requests_unixsocket
35import os33import os
36import sys
37import time
38from collections import namedtuple34from collections import namedtuple
3935
36from checkbox_support.snap_utils.snapd import Snapd
37import requests
4038
41Connection = namedtuple(39Connection = namedtuple(
42 'Connection',40 'Connection',
43 ['target_snap', 'target_slot', 'plug_snap', 'plug_plug'])41 ['target_snap', 'target_slot', 'plug_snap', 'plug_plug'])
4442
4543
46class BadRequest(Exception):44def get_connections():
47 pass45 data = Snapd().interfaces()
46 connections = []
47 if 'plugs' in data:
48 for plug in data['plugs']:
49 if 'connections' in plug:
50 for con in plug['connections']:
51 connections.append(Connection(
52 con['snap'], con['slot'],
53 plug['snap'], plug['plug']))
54 return connections
4855
4956
50def main():57def main():
@@ -53,77 +60,23 @@ def main():
53 'connections', nargs='+', default=[],60 'connections', nargs='+', default=[],
54 metavar='plug:target_snap:target_slot')61 metavar='plug:target_snap:target_slot')
55 args = parser.parse_args()62 args = parser.parse_args()
56 api = SnapdAPI()
5763
58 for conn in [spec.split(':') for spec in args.connections]:64 for conn in [spec.split(':') for spec in args.connections]:
59 if len(conn) != 3:65 if len(conn) != 3:
60 raise SystemExit("Bad connection description")66 raise SystemExit("Bad connection description")
61 assert os.environ['SNAP_NAME']67 assert os.environ['SNAP_NAME']
62 snap = os.environ['SNAP_NAME']68 snap = os.environ['SNAP_NAME']
63 existing_connections = api.get_connections()69 existing_connections = get_connections()
64 new_connection = Connection(70 new_connection = Connection(
65 target_snap=conn[1], target_slot=conn[2],71 target_snap=conn[1], target_slot=conn[2],
66 plug_snap=snap, plug_plug=conn[0])72 plug_snap=snap, plug_plug=conn[0])
67 if new_connection not in existing_connections:73 if new_connection not in existing_connections:
68 try:74 try:
69 api.connect(new_connection)75 Snapd().connect(*new_connection)
70 except BadRequest as exc:76 except requests.HTTPError as exc:
71 logging.warning("Failed to connect %s to %s. %s" % (77 logging.warning("Failed to connect %s to %s. %s" % (
72 conn[0], conn[1], exc))78 conn[0], conn[1], exc))
7379
7480
75class SnapdAPI():
76 """Based on https://github.com/snapcore/snapd/wiki/REST-API"""
77 SNAPD_BASE_URL = 'http+unix://%2Frun%2Fsnapd.socket'
78
79 def __init__(self):
80 self.session = requests_unixsocket.Session()
81
82 def get_connections(self):
83 data = self._get('/v2/interfaces')
84 connections = []
85 if 'plugs' in data:
86 for plug in data['plugs']:
87 if 'connections' in plug:
88 for con in plug['connections']:
89 connections.append(Connection(
90 con['snap'], con['slot'],
91 plug['snap'], plug['plug']))
92 return connections
93
94 def connect(self, con):
95 json_data = json.dumps({
96 'action': 'connect',
97 'slots': [{'snap': con.target_snap, 'slot': con.target_slot}],
98 'plugs': [{'snap': con.plug_snap, 'plug': con.plug_plug}]
99 })
100 res = self._post('/v2/interfaces', json_data)
101 ready = False
102 while not ready:
103 # busy wait until snapd reports connection job as finised
104 time.sleep(0.5)
105 con_res = self._get('/v2/changes/{}'.format(res['change']))
106 ready = con_res['ready']
107
108 def _get(self, path):
109 res = self.session.get(self.SNAPD_BASE_URL + path)
110 if not res.ok:
111 logging.error("Got error %i attempting to access %s",
112 res.status_code, path)
113 sys.exit(1)
114 return res.json()['result']
115
116 def _post(self, path, data=None):
117 res = self.session.post(self.SNAPD_BASE_URL + path, data=data)
118 if not res.ok:
119 full_res = json.loads(res.text)
120 if res.status_code == 400:
121 raise BadRequest(full_res['result']['message'])
122 logging.error("Got error %i attempting to access %s",
123 res.status_code, path)
124 sys.exit(1)
125 return res.json()
126
127
128if __name__ == '__main__':81if __name__ == '__main__':
129 main()82 main()
diff --git a/checkbox_support/snap_utils/config.py b/checkbox_support/snap_utils/config.py
index c33bafc..98083be 100644
--- a/checkbox_support/snap_utils/config.py
+++ b/checkbox_support/snap_utils/config.py
@@ -12,52 +12,6 @@ import re
12import subprocess12import subprocess
13import sys13import sys
1414
15import requests
16import requests_unixsocket
17
18SNAPD_BASE_URL = 'http+unix://%2Frun%2Fsnapd.socket'
19
20
21class SnapdQuery():
22
23 def __init__(self):
24 self._session = requests_unixsocket.Session()
25
26 def get(self, path, params=None):
27 r = self._session.get(SNAPD_BASE_URL + path, params=params)
28 if r.status_code != requests.codes.ok:
29 raise SystemExit('Got error {} attempting to access {}\n'.format(
30 r.status_code, path))
31 return r
32
33 def post(self, path, data=None):
34 res = self._session.post(SNAPD_BASE_URL + path, data=data)
35 if not res.ok:
36 raise SystemExit('Got error {} attempting to post to {}\n'.format(
37 res.status_code, path))
38 return res.json()
39
40 def put(self, path, data=None):
41 res = self._session.put(SNAPD_BASE_URL + path, data=data)
42 if not res.ok:
43 raise SystemExit('Got error {} attempting to put to {}\n'.format(
44 res.status_code, path))
45 return res.json()
46
47
48def get_configuration(snap, key):
49 path = '/v2/snaps/{}/conf'.format(snap)
50 params = 'keys={}'.format(key)
51 query = SnapdQuery()
52 return query.get(path, params).json()['result'][key]
53
54
55def set_configuration(snap, key, value):
56 path = '/v2/snaps/{}/conf'.format(snap)
57 json_data = json.dumps({key: value})
58 query = SnapdQuery()
59 return query.put(path, json_data)['status']
60
6115
62def get_snapctl_config(keys):16def get_snapctl_config(keys):
63 """Query snapctl for given keys."""17 """Query snapctl for given keys."""
@@ -95,8 +49,8 @@ def get_configuration_set():
95 k = k.replace('_', '-').lower()49 k = k.replace('_', '-').lower()
96 config_set[k] = v50 config_set[k] = v
97 except FileNotFoundError:51 except FileNotFoundError:
98 # silently ignore missing config_vars52 # silently ignore missing config_vars
99 pass53 pass
100 return config_set54 return config_set
10155
10256
diff --git a/checkbox_support/snap_utils/snapd.py b/checkbox_support/snap_utils/snapd.py
103new file mode 10064457new file mode 100644
index 0000000..6987a29
--- /dev/null
+++ b/checkbox_support/snap_utils/snapd.py
@@ -0,0 +1,146 @@
1# Copyright 2019 Canonical Ltd.
2# All rights reserved.
3#
4# Written by:
5# Jonathan Cave <jonathan.cave@canonical.com>
6
7import json
8import time
9
10import requests_unixsocket
11
12
13class AsyncException(Exception):
14
15 def __init__(self, message):
16 self.message = message
17
18
19class Snapd():
20
21 _url = 'http+unix://%2Frun%2Fsnapd.socket'
22
23 _snaps = '/v2/snaps'
24 _find = '/v2/find'
25 _changes = '/v2/changes'
26 _system_info = '/v2/system-info'
27 _interfaces = '/v2/interfaces'
28 _assertions = '/v2/assertions'
29
30 def __init__(self):
31 self._session = requests_unixsocket.Session()
32
33 def _get(self, path, params=None, decode=True):
34 r = self._session.get(self._url + path, params=params)
35 r.raise_for_status()
36 if decode:
37 return r.json()
38 return r
39
40 def _post(self, path, data=None, decode=True):
41 r = self._session.post(self._url + path, data=data)
42 r.raise_for_status()
43 if decode:
44 return r.json()
45 return r
46
47 def _put(self, path, data=None, decode=True):
48 r = self._session.put(self._url + path, data=data)
49 r.raise_for_status()
50 if decode:
51 return r.json()
52 return r
53
54 def _poll_change(self, change_id, timeout=30):
55 for _ in range(30):
56 status = self.change(change_id)
57 if status == 'Done':
58 return True
59 time.sleep(1)
60 raise AsyncException(status)
61
62 def list(self, snap=None):
63 path = self._snaps
64 if snap is not None:
65 path += '/' + snap
66 return self._get(path)['result']
67
68 def install(self, snap, channel='stable', revision=None):
69 path = self._snaps + '/' + snap
70 data = {'action': 'install', 'channel': channel}
71 if revision is not None:
72 data['revision'] = revision
73 r = self._post(path, json.dumps(data))
74 if r['type'] == 'async' and r['status'] == 'Accepted':
75 self._poll_change(r['change'])
76
77 def remove(self, snap, revision=None):
78 path = self._snaps + '/' + snap
79 data = {'action': 'remove'}
80 if revision is not None:
81 data['revision'] = revision
82 r = self._post(path, json.dumps(data))
83 if r['type'] == 'async' and r['status'] == 'Accepted':
84 self._poll_change(r['change'])
85
86 def find(self, search, exact=False):
87 if exact:
88 p = 'name={}'.format(search)
89 else:
90 p = 'q={}'.format(search)
91 return self._get(self._find, params=p)['result']
92
93 def info(self, snap):
94 return self.find(snap, exact=True)[0]
95
96 def refresh(self, snap, channel='stable', revision=None):
97 path = self._snaps + '/' + snap
98 data = {'action': 'refresh', 'channel': channel}
99 if revision is not None:
100 data['revision'] = revision
101 r = self._post(path, json.dumps(data))
102 if r['type'] == 'async' and r['status'] == 'Accepted':
103 self._poll_change(r['change'])
104
105 def change(self, change_id):
106 path = self._changes + '/' + change_id
107 r = self._get(path)
108 return r['result']['status']
109
110 def revert(self, snap, channel='stable', revision=None):
111 path = self._snaps + '/' + snap
112 data = {'action': 'revert', 'channel': channel}
113 if revision is not None:
114 data['revision'] = revision
115 r = self._post(path, json.dumps(data))
116 if r['type'] == 'async' and r['status'] == 'Accepted':
117 self._poll_change(r['change'])
118
119 def get_configuration(self, snap, key):
120 path = self._snaps + '/' + snap + '/conf'
121 p = 'keys={}'.format(key)
122 return self._get(path, params=p)['result'][key]
123
124 def set_configuration(self, snap, key, value):
125 path = self._snaps + '/' + snap + '/conf'
126 data = {key: value}
127 r = self._post(path, json.dumps(data))
128 if r['type'] == 'async' and r['status'] == 'Accepted':
129 self._poll_change(r['change'])
130
131 def interfaces(self):
132 return self._get(self._interfaces)['result']
133
134 def connect(self, slot_snap, slot_slot, plug_snap, plug_plug):
135 data = {
136 'action': 'connect',
137 'slots': [{'snap': slot_snap, 'slot': slot_slot}],
138 'plugs': [{'snap': plug_snap, 'plug': plug_plug}]
139 }
140 r = self._post(self._interfaces, json.dumps(data))
141 if r['type'] == 'async' and r['status'] == 'Accepted':
142 self._poll_change(r['change'])
143
144 def get_assertions(self, assertion_type):
145 path = self._assertions + '/' + assertion_type
146 return self._get(path, decode=False)
diff --git a/setup.py b/setup.py
index dd2ed8c..3c3ac85 100755
--- a/setup.py
+++ b/setup.py
@@ -82,8 +82,6 @@ setup(
82 "checkbox_support.scripts.fwts_test:main"),82 "checkbox_support.scripts.fwts_test:main"),
83 ("checkbox-support-usb_read_write="83 ("checkbox-support-usb_read_write="
84 "checkbox_support.scripts.usb_read_write:run_read_write_test"),84 "checkbox_support.scripts.usb_read_write:run_read_write_test"),
85 ("checkbox-support-snap_configuration="
86 "checkbox_support.scripts.snap_configuration:main"),
87 ("checkbox-support-nmea_test="85 ("checkbox-support-nmea_test="
88 "checkbox_support.scripts.nmea_test:main"),86 "checkbox_support.scripts.nmea_test:main"),
89 ("checkbox-support-snap_connect="87 ("checkbox-support-snap_connect="

Subscribers

People subscribed via source and target branches