Merge ~jocave/plainbox-provider-resource:simplify-snapd-resource into plainbox-provider-resource:master

Proposed by Jonathan Cave
Status: Merged
Approved by: Jonathan Cave
Approved revision: 0066655d64cbc236a987a15d2e2f927c4437529a
Merged at revision: b715b0b22b91138b9a4749e286435ccefd7b5dd4
Proposed branch: ~jocave/plainbox-provider-resource:simplify-snapd-resource
Merge into: plainbox-provider-resource:master
Diff against target: 310 lines (+60/-177)
1 file modified
bin/snapd_resource (+60/-177)
Reviewer Review Type Date Requested Status
Sylvain Pineau (community) Approve
Review via email: mp+365455@code.launchpad.net

Description of the change

Use the new snapd class in checkbox-support to simplify the snapd_resource script.

Tested that all the resource jobs that use this script work as before.

Depends on https://code.launchpad.net/~jocave/checkbox-support/+git/checkbox-support/+merge/365454

To post a comment you must log in.
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

Also tested all jobs using snapd_resource, +1.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/bin/snapd_resource b/bin/snapd_resource
index d88ffa0..234ccd8 100755
--- a/bin/snapd_resource
+++ b/bin/snapd_resource
@@ -5,100 +5,50 @@
5# Written by:5# Written by:
6# Authors: Jonathan Cave <jonathan.cave@canonical.com>6# Authors: Jonathan Cave <jonathan.cave@canonical.com>
77
8import json
9import io8import io
10import logging
11import os
12import string9import string
13import sys
14import time
15import traceback
1610
17from guacamole import Command11from guacamole import Command
18import requests12from checkbox_support.snap_utils.snapd import Snapd
19import requests_unixsocket
2013
21from collections import namedtuple14from collections import namedtuple
2215
2316
24SNAPD_BASE_URL = 'http+unix://%2Frun%2Fsnapd.socket'17def slugify(_string):
2518 """Transform string to one that can be used as the key in a resource job"""
2619 valid_chars = frozenset(
27class SnapdQuery(Command):20 "_{}{}".format(string.ascii_letters, string.digits))
2821 return ''.join(c if c in valid_chars else '_' for c in _string)
29 def __init__(self):22
30 self._session = requests_unixsocket.Session()23
3124def http_to_resource(assertion_stream):
32 def get(self, path):25 """ Super naive Assertion parser
33 try:26
34 r = self._session.get(SNAPD_BASE_URL + path)27 No attempt to handle assertions with a body. Discards signatures based
35 except requests.exceptions.RequestException as e:28 on lack of colon characters. Update: need to be less naive about
36 # snapd might not be installed, silently return an empty Response29 gadget and kernel names on UC18
37 if 'FileNotFoundError' in traceback.format_exc():30 """
38 r = requests.Response()31 count = int(assertion_stream.headers['X-Ubuntu-Assertions-Count'])
39 r.status_code = 20032 if count > 0:
40 r._content = b'{"result":""}'33 for line in io.StringIO(assertion_stream.text):
41 r.headers['X-Ubuntu-Assertions-Count'] = 034 if line.strip() == "":
42 else:35 print()
43 raise36 if line.count(':') == 1:
44 if r.status_code != requests.codes.ok:37 key, val = [x.strip() for x in line.split(':')]
45 logging.error("Got error {} attempting to access {}".format(38 if key in ('gadget', 'kernel'):
46 r.status_code, path))39 if '=' in val:
47 sys.exit(1)40 snap, track = [x.strip() for x in val.split('=')]
48 return r41 print('{}: {}'.format(key, snap))
4942 print('{}_track: {}'.format(key, track))
50 def post(self, path, data=None):43 continue
51 try:44 print(line.strip())
52 res = self._session.post(SNAPD_BASE_URL + path, data=data)45 return count
53 except requests.exceptions.RequestException as e:46
54 # snapd might not be installed, silently return an empty Response47
55 if 'FileNotFoundError' in traceback.format_exc():48class ModelAssertion(Command):
56 r = requests.Response()
57 r.status_code = 200
58 r._content = b'{"result":""}'
59 r.headers['X-Ubuntu-Assertions-Count'] = 0
60 else:
61 raise
62 if not res.ok:
63 logging.error("Got error %i attempting to access %s",
64 res.status_code, path)
65 sys.exit(1)
66 return res.json()
67
68
69class AssertionQuery(SnapdQuery):
70
71 prefix = '/v2/assertions/'
72
73 def convert(self, assertion):
74 """ Super naive Assertion parser
75
76 No attempt to handle assertions with a body. Discards signatures based
77 on lack of colon characters. Update: need to be less naive about
78 gadget and kernel names on UC18
79 """
80 data = self.get(self.prefix + assertion)
81 count = int(data.headers['X-Ubuntu-Assertions-Count'])
82 if count > 0:
83 for line in io.StringIO(data.text):
84 if line.strip() == "":
85 print()
86 if line.count(':') == 1:
87 key, val = [x.strip() for x in line.split(':')]
88 if key in ('gadget', 'kernel'):
89 if '=' in val:
90 snap, track = [x.strip() for x in val.split('=')]
91 print('{}: {}'.format(key, snap))
92 print('{}_track: {}'.format(key, track))
93 continue
94 print(line.strip())
95 return count
96
97
98class ModelAssertion(AssertionQuery):
9949
100 def invoked(self, ctx):50 def invoked(self, ctx):
101 count = self.convert('model')51 count = http_to_resource(Snapd().get_assertions('model'))
102 if count == 0:52 if count == 0:
103 # Print a dummy assertion - not nice but really trick to use53 # Print a dummy assertion - not nice but really trick to use
104 # plainbox resources without some defualt value54 # plainbox resources without some defualt value
@@ -108,10 +58,10 @@ class ModelAssertion(AssertionQuery):
108 print()58 print()
10959
11060
111class SerialAssertion(AssertionQuery):61class SerialAssertion(Command):
11262
113 def invoked(self, ctx):63 def invoked(self, ctx):
114 count = self.convert('serial')64 count = http_to_resource(Snapd().get_assertions('serial'))
115 if count == 0:65 if count == 0:
116 # Print a dummy assertion - not nice but really trick to use66 # Print a dummy assertion - not nice but really trick to use
117 # plainbox resources without some defualt value67 # plainbox resources without some defualt value
@@ -129,14 +79,11 @@ class Assertions(Command):
129 )79 )
13080
13181
132class Snaps(SnapdQuery):82class Snaps(Command):
133
134 prefix = '/v2/snaps'
13583
136 def invoked(self, ctx):84 def invoked(self, ctx):
137 data = self.get(self.prefix).json()85 data = Snapd().list()
13886 for snap in data:
139 for snap in data['result']:
140 def print_field(key):87 def print_field(key):
141 try:88 try:
142 val = snap[key]89 val = snap[key]
@@ -153,25 +100,13 @@ class Snaps(SnapdQuery):
153 print()100 print()
154101
155102
156class InterfacesQuery(SnapdQuery):103class Endpoints(Command):
157
158 prefix = '/v2/interfaces'
159
160
161def slugify(_string):
162 """Transform string to one that can be used as the key in a resource job"""
163 valid_chars = frozenset(
164 "_{}{}".format(string.ascii_letters, string.digits))
165 return ''.join(c if c in valid_chars else '_' for c in _string)
166
167
168class Endpoints(InterfacesQuery):
169104
170 def invoked(self, ctx):105 def invoked(self, ctx):
171 data = self.get(self.prefix).json()106 data = Snapd().interfaces()
172107
173 if 'plugs' in data['result']:108 if 'plugs' in data:
174 for plug in data['result']['plugs']:109 for plug in data['plugs']:
175 def print_field(key):110 def print_field(key):
176 val = plug[key]111 val = plug[key]
177 if val != '':112 if val != '':
@@ -186,8 +121,8 @@ class Endpoints(InterfacesQuery):
186 print('attr_{}: {}'.format(slugify(attr), val))121 print('attr_{}: {}'.format(slugify(attr), val))
187 print()122 print()
188123
189 if 'slots' in data['result']:124 if 'slots' in data:
190 for slot in data['result']['slots']:125 for slot in data['slots']:
191 def print_field(key):126 def print_field(key):
192 val = slot[key]127 val = slot[key]
193 if val != '':128 if val != '':
@@ -208,85 +143,33 @@ Connection = namedtuple(
208 ['target_snap', 'target_slot', 'plug_snap', 'plug_plug'])143 ['target_snap', 'target_slot', 'plug_snap', 'plug_plug'])
209144
210145
211class Connections(InterfacesQuery):146def get_connections():
147 data = Snapd().interfaces()
148 connections = []
149 if 'plugs' in data:
150 for plug in data['plugs']:
151 if 'connections' in plug:
152 for con in plug['connections']:
153 connections.append(Connection(
154 con['snap'], con['slot'],
155 plug['snap'], plug['plug']))
156 return connections
157
158
159class Connections(Command):
212160
213 def invoked(self, ctx):161 def invoked(self, ctx):
214 for conn in self.get_connections():162 for conn in get_connections():
215 print('slot: {}:{}'.format(conn.target_snap, conn.target_slot))163 print('slot: {}:{}'.format(conn.target_snap, conn.target_slot))
216 print('plug: {}:{}'.format(conn.plug_snap, conn.plug_plug))164 print('plug: {}:{}'.format(conn.plug_snap, conn.plug_plug))
217 print()165 print()
218166
219 def get_connections(self):
220 data = self.get('/v2/interfaces').json()['result']
221 connections = []
222 if 'plugs' in data:
223 for plug in data['plugs']:
224 if 'connections' in plug:
225 for con in plug['connections']:
226 connections.append(Connection(
227 con['snap'], con['slot'],
228 plug['snap'], plug['plug']))
229 return connections
230
231
232class Connect(Connections):
233 """
234 Connect 'current snap' to other snaps.
235
236 The syntax of the command is A:B:C, where:
237 A - plug to be connected
238 B - target snap (snap the plug will connect to)
239 C - target slot (slot to connect to)
240
241 Note that originating snap is implied. $SNAP_NAME is used.
242
243 Example:
244
245 $ sudo snapd_resource interfaces connect \
246 udisks2:udisks2:service
247
248 Note that the program needs sudo to connect plugs.
249 """
250
251 def register_arguments(self, parser):
252 parser.add_argument(
253 'connection', nargs='+', default=[],
254 metavar='plug:target_snap:target_slot')
255
256 def invoked(self, ctx):
257 for conn in [spec.split(':') for spec in ctx.args.connection]:
258 if len(conn) != 3:
259 raise SystemExit("Bad connection description")
260 assert os.environ['SNAP_NAME']
261 snap = os.environ['SNAP_NAME']
262 existing_connections = self.get_connections()
263 new_connection = Connection(
264 target_snap=conn[1], target_slot=conn[2],
265 plug_snap=snap, plug_plug=conn[0])
266 if new_connection not in existing_connections:
267 self.connect(new_connection)
268
269 def connect(self, con):
270 json_data = json.dumps({
271 'action': 'connect',
272 'slots': [{'snap': con.target_snap, 'slot': con.target_slot}],
273 'plugs': [{'snap': con.plug_snap, 'plug': con.plug_plug}]
274 })
275 res = self.post('/v2/interfaces', json_data)
276 ready = False
277 while not ready:
278 # busy wait until snapd reports connection job as finised
279 time.sleep(0.5)
280 con_res = self.get('/v2/changes/{}'.format(res['change']))
281 ready = con_res.json()['result']['ready']
282
283167
284class Interfaces(Command):168class Interfaces(Command):
285169
286 sub_commands = (170 sub_commands = (
287 ('endpoints', Endpoints),171 ('endpoints', Endpoints),
288 ('connections', Connections),172 ('connections', Connections),
289 ('connect', Connect),
290 )173 )
291174
292175

Subscribers

People subscribed via source and target branches