Merge lp:~cjwatson/launchpad-buildd/local-snap-proxy into lp:launchpad-buildd
- local-snap-proxy
- Merge into trunk
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Merged at revision: | 341 | ||||||||
Proposed branch: | lp:~cjwatson/launchpad-buildd/local-snap-proxy | ||||||||
Merge into: | lp:launchpad-buildd | ||||||||
Diff against target: |
598 lines (+370/-41) 10 files modified
buildd-genconfig (+6/-1) debian/changelog (+4/-0) debian/control (+1/-0) debian/postinst (+1/-1) debian/upgrade-config (+13/-0) lpbuildd/buildd-slave.tac (+1/-0) lpbuildd/snap.py (+262/-2) lpbuildd/target/build_snap.py (+0/-37) lpbuildd/tests/test_snap.py (+79/-0) template-buildd-slave.conf (+3/-0) |
||||||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad-buildd/local-snap-proxy | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+322545@code.launchpad.net |
Commit message
Add a local unauthenticated proxy on port 8222, which proxies through to
the remote authenticated proxy. This should allow running a wider range
of network clients, since some of them apparently don't support
authenticated proxies very well.
Description of the change
Colin Watson (cjwatson) wrote : | # |
Adam Collard (adam-collard) wrote : | # |
drive by review
- 217. By Colin Watson
-
Remove dead code.
Colin Watson (cjwatson) : | # |
Kevin W Monroe (kwmonroe) wrote : | # |
Aside from the merge conflicts, the spirit of this gets a big +1 from me. I'm running into all sorts of trouble with java projects (mvn, ant, ivy, gradle) and their poor support for auth'd proxies. I think an unauth'd alternative would be super useful.
- 218. By Colin Watson
-
Merge trunk.
- 219. By Colin Watson
-
Drop now-unnecessary compatibility with Twisted < 14.0.0.
Brett Sutton (bsutton) wrote : | # |
Just to add my voice.
This is currently blocking a number of apps I'm trying to build in the store plus anyone that uses a remote part for Apache Tomcat with SSL that I've built.
William Grant (wgrant) wrote : | # |
This seems to work fine with some manual testing.
However, this won't directly work for snap builds any more: that --proxy-url is going to point to the container rather than the host.
- 220. By Colin Watson
-
Merge trunk.
- 221. By Colin Watson
-
Build-depend on curl for proxy tests.
- 222. By Colin Watson
-
Fix version in upgrade-config.
- 223. By Colin Watson
-
Remove duplicate --proxy-url option.
- 224. By Colin Watson
-
Point --proxy-url to the LXD host.
- 225. By Colin Watson
-
Close LP #1690834 and #1753340.
Preview Diff
1 | === modified file 'buildd-genconfig' | |||
2 | --- buildd-genconfig 2016-12-09 18:04:00 +0000 | |||
3 | +++ buildd-genconfig 2018-06-10 12:47:59 +0000 | |||
4 | @@ -37,6 +37,11 @@ | |||
5 | 37 | metavar="FILE", | 37 | metavar="FILE", |
6 | 38 | default="/usr/share/launchpad-buildd/template-buildd-slave.conf") | 38 | default="/usr/share/launchpad-buildd/template-buildd-slave.conf") |
7 | 39 | 39 | ||
8 | 40 | parser.add_option("--snap-proxy-port", dest="SNAPPROXYPORT", | ||
9 | 41 | help="the port the local snap proxy binds to", | ||
10 | 42 | metavar="PORT", | ||
11 | 43 | default="8222") | ||
12 | 44 | |||
13 | 40 | (options, args) = parser.parse_args() | 45 | (options, args) = parser.parse_args() |
14 | 41 | 46 | ||
15 | 42 | template = open(options.TEMPLATE, "r").read() | 47 | template = open(options.TEMPLATE, "r").read() |
16 | @@ -46,6 +51,7 @@ | |||
17 | 46 | "@BINDHOST@": options.BINDHOST, | 51 | "@BINDHOST@": options.BINDHOST, |
18 | 47 | "@ARCHTAG@": options.ARCHTAG, | 52 | "@ARCHTAG@": options.ARCHTAG, |
19 | 48 | "@BINDPORT@": options.BINDPORT, | 53 | "@BINDPORT@": options.BINDPORT, |
20 | 54 | "@SNAPPROXYPORT@": options.SNAPPROXYPORT, | ||
21 | 49 | } | 55 | } |
22 | 50 | 56 | ||
23 | 51 | for replacement_key in replacements: | 57 | for replacement_key in replacements: |
24 | @@ -53,4 +59,3 @@ | |||
25 | 53 | replacements[replacement_key]) | 59 | replacements[replacement_key]) |
26 | 54 | 60 | ||
27 | 55 | print(template.strip()) | 61 | print(template.strip()) |
28 | 56 | |||
29 | 57 | 62 | ||
30 | === modified file 'debian/changelog' | |||
31 | --- debian/changelog 2018-06-05 02:06:04 +0000 | |||
32 | +++ debian/changelog 2018-06-10 12:47:59 +0000 | |||
33 | @@ -10,6 +10,10 @@ | |||
34 | 10 | * Refactor VCS operations from lpbuildd.target.build_snap out to a module | 10 | * Refactor VCS operations from lpbuildd.target.build_snap out to a module |
35 | 11 | that can be used by other targets. | 11 | that can be used by other targets. |
36 | 12 | * Allow checking out a git tag rather than a branch (LP: #1687078). | 12 | * Allow checking out a git tag rather than a branch (LP: #1687078). |
37 | 13 | * Add a local unauthenticated proxy on port 8222, which proxies through to | ||
38 | 14 | the remote authenticated proxy. This should allow running a wider range | ||
39 | 15 | of network clients, since some of them apparently don't support | ||
40 | 16 | authenticated proxies very well (LP: #1690834, #1753340). | ||
41 | 13 | 17 | ||
42 | 14 | -- Colin Watson <cjwatson@ubuntu.com> Tue, 08 May 2018 10:36:22 +0100 | 18 | -- Colin Watson <cjwatson@ubuntu.com> Tue, 08 May 2018 10:36:22 +0100 |
43 | 15 | 19 | ||
44 | 16 | 20 | ||
45 | === modified file 'debian/control' | |||
46 | --- debian/control 2017-11-27 17:14:19 +0000 | |||
47 | +++ debian/control 2018-06-10 12:47:59 +0000 | |||
48 | @@ -5,6 +5,7 @@ | |||
49 | 5 | Standards-Version: 3.9.5 | 5 | Standards-Version: 3.9.5 |
50 | 6 | Build-Depends: apt-utils, | 6 | Build-Depends: apt-utils, |
51 | 7 | bzr, | 7 | bzr, |
52 | 8 | curl, | ||
53 | 8 | debhelper (>= 9~), | 9 | debhelper (>= 9~), |
54 | 9 | dh-exec, | 10 | dh-exec, |
55 | 10 | dh-python, | 11 | dh-python, |
56 | 11 | 12 | ||
57 | === modified file 'debian/postinst' | |||
58 | --- debian/postinst 2017-08-29 13:19:35 +0000 | |||
59 | +++ debian/postinst 2018-06-10 12:47:59 +0000 | |||
60 | @@ -14,7 +14,7 @@ | |||
61 | 14 | 14 | ||
62 | 15 | make_buildd() | 15 | make_buildd() |
63 | 16 | { | 16 | { |
65 | 17 | /usr/share/launchpad-buildd/buildd-genconfig --name=default --host=0.0.0.0 --port=8221 > \ | 17 | /usr/share/launchpad-buildd/buildd-genconfig --name=default --host=0.0.0.0 --port=8221 --snap-proxy-port=8222 > \ |
66 | 18 | /etc/launchpad-buildd/default | 18 | /etc/launchpad-buildd/default |
67 | 19 | echo Default buildd created. | 19 | echo Default buildd created. |
68 | 20 | } | 20 | } |
69 | 21 | 21 | ||
70 | === modified file 'debian/upgrade-config' | |||
71 | --- debian/upgrade-config 2016-12-09 18:05:21 +0000 | |||
72 | +++ debian/upgrade-config 2018-06-10 12:47:59 +0000 | |||
73 | @@ -197,6 +197,17 @@ | |||
74 | 197 | in_file.close() | 197 | in_file.close() |
75 | 198 | out_file.close() | 198 | out_file.close() |
76 | 199 | 199 | ||
77 | 200 | def upgrade_to_162(): | ||
78 | 201 | print("Upgrading %s to version 162" % conf_file) | ||
79 | 202 | os.rename(conf_file, conf_file + "-prev162~") | ||
80 | 203 | |||
81 | 204 | with open(conf_file + "-prev162~", "r") as in_file: | ||
82 | 205 | with open(conf_file, "w") as out_file: | ||
83 | 206 | out_file.write(in_file.read()) | ||
84 | 207 | out_file.write( | ||
85 | 208 | "\n[snapmanager]\n" | ||
86 | 209 | "proxyport = 8222\n") | ||
87 | 210 | |||
88 | 200 | if __name__ == "__main__": | 211 | if __name__ == "__main__": |
89 | 201 | old_version = re.sub(r'[~-].*', '', old_version) | 212 | old_version = re.sub(r'[~-].*', '', old_version) |
90 | 202 | if apt_pkg.version_compare(old_version, "12") < 0: | 213 | if apt_pkg.version_compare(old_version, "12") < 0: |
91 | @@ -223,3 +234,5 @@ | |||
92 | 223 | upgrade_to_126() | 234 | upgrade_to_126() |
93 | 224 | if apt_pkg.version_compare(old_version, "127") < 0: | 235 | if apt_pkg.version_compare(old_version, "127") < 0: |
94 | 225 | upgrade_to_127() | 236 | upgrade_to_127() |
95 | 237 | if apt_pkg.version_compare(old_version, "162") < 0: | ||
96 | 238 | upgrade_to_162() | ||
97 | 226 | 239 | ||
98 | === modified file 'lpbuildd/buildd-slave.tac' | |||
99 | --- lpbuildd/buildd-slave.tac 2018-02-27 13:25:36 +0000 | |||
100 | +++ lpbuildd/buildd-slave.tac 2018-06-10 12:47:59 +0000 | |||
101 | @@ -51,6 +51,7 @@ | |||
102 | 51 | application.addComponent( | 51 | application.addComponent( |
103 | 52 | RotatableFileLogObserver(options.get('logfile')), ignoreClass=1) | 52 | RotatableFileLogObserver(options.get('logfile')), ignoreClass=1) |
104 | 53 | builddslaveService = service.IServiceCollection(application) | 53 | builddslaveService = service.IServiceCollection(application) |
105 | 54 | slave.slave.service = builddslaveService | ||
106 | 54 | 55 | ||
107 | 55 | root = resource.Resource() | 56 | root = resource.Resource() |
108 | 56 | root.putChild('rpc', slave) | 57 | root.putChild('rpc', slave) |
109 | 57 | 58 | ||
110 | === modified file 'lpbuildd/snap.py' | |||
111 | --- lpbuildd/snap.py 2018-04-21 10:04:37 +0000 | |||
112 | +++ lpbuildd/snap.py 2018-06-10 12:47:59 +0000 | |||
113 | @@ -5,9 +5,39 @@ | |||
114 | 5 | 5 | ||
115 | 6 | __metaclass__ = type | 6 | __metaclass__ = type |
116 | 7 | 7 | ||
117 | 8 | import base64 | ||
118 | 9 | import io | ||
119 | 8 | import json | 10 | import json |
120 | 9 | import os | 11 | import os |
121 | 10 | import sys | 12 | import sys |
122 | 13 | try: | ||
123 | 14 | from urllib.error import ( | ||
124 | 15 | HTTPError, | ||
125 | 16 | URLError, | ||
126 | 17 | ) | ||
127 | 18 | from urllib.parse import urlparse | ||
128 | 19 | from urllib.request import ( | ||
129 | 20 | Request, | ||
130 | 21 | urlopen, | ||
131 | 22 | ) | ||
132 | 23 | except ImportError: | ||
133 | 24 | from urllib2 import ( | ||
134 | 25 | HTTPError, | ||
135 | 26 | Request, | ||
136 | 27 | URLError, | ||
137 | 28 | urlopen, | ||
138 | 29 | ) | ||
139 | 30 | from urlparse import urlparse | ||
140 | 31 | |||
141 | 32 | from twisted.application import strports | ||
142 | 33 | from twisted.internet import reactor | ||
143 | 34 | from twisted.internet.interfaces import IHalfCloseableProtocol | ||
144 | 35 | from twisted.python.compat import intToBytes | ||
145 | 36 | from twisted.web import ( | ||
146 | 37 | http, | ||
147 | 38 | proxy, | ||
148 | 39 | ) | ||
149 | 40 | from zope.interface import implementer | ||
150 | 11 | 41 | ||
151 | 12 | from lpbuildd.debian import ( | 42 | from lpbuildd.debian import ( |
152 | 13 | DebianBuildManager, | 43 | DebianBuildManager, |
153 | @@ -21,6 +51,195 @@ | |||
154 | 21 | RETCODE_FAILURE_BUILD = 201 | 51 | RETCODE_FAILURE_BUILD = 201 |
155 | 22 | 52 | ||
156 | 23 | 53 | ||
157 | 54 | class SnapProxyClient(proxy.ProxyClient): | ||
158 | 55 | |||
159 | 56 | def __init__(self, command, rest, version, headers, data, father): | ||
160 | 57 | proxy.ProxyClient.__init__( | ||
161 | 58 | self, command, rest, version, headers, data, father) | ||
162 | 59 | # Why doesn't ProxyClient at least store this? | ||
163 | 60 | self.version = version | ||
164 | 61 | # We must avoid calling self.father.finish in the event that its | ||
165 | 62 | # connection was already lost, i.e. if the original client | ||
166 | 63 | # disconnects first (which is particularly likely in the case of | ||
167 | 64 | # CONNECT). | ||
168 | 65 | d = self.father.notifyFinish() | ||
169 | 66 | d.addBoth(self.requestFinished) | ||
170 | 67 | |||
171 | 68 | def connectionMade(self): | ||
172 | 69 | proxy.ProxyClient.connectionMade(self) | ||
173 | 70 | self.father.setChildClient(self) | ||
174 | 71 | |||
175 | 72 | def sendCommand(self, command, path): | ||
176 | 73 | # For some reason, HTTPClient.sendCommand doesn't preserve the | ||
177 | 74 | # protocol version. | ||
178 | 75 | self.transport.writeSequence( | ||
179 | 76 | [command, b' ', path, b' ', self.version, b'\r\n']) | ||
180 | 77 | |||
181 | 78 | def handleEndHeaders(self): | ||
182 | 79 | self.father.handleEndHeaders() | ||
183 | 80 | |||
184 | 81 | def sendData(self, data): | ||
185 | 82 | self.transport.write(data) | ||
186 | 83 | |||
187 | 84 | def endData(self): | ||
188 | 85 | if self.transport is not None: | ||
189 | 86 | self.transport.loseWriteConnection() | ||
190 | 87 | |||
191 | 88 | def requestFinished(self, result): | ||
192 | 89 | self._finished = True | ||
193 | 90 | self.transport.loseConnection() | ||
194 | 91 | |||
195 | 92 | |||
196 | 93 | class SnapProxyClientFactory(proxy.ProxyClientFactory): | ||
197 | 94 | |||
198 | 95 | protocol = SnapProxyClient | ||
199 | 96 | |||
200 | 97 | |||
201 | 98 | class SnapProxyRequest(http.Request): | ||
202 | 99 | |||
203 | 100 | child_client = None | ||
204 | 101 | _request_buffer = None | ||
205 | 102 | _request_data_done = False | ||
206 | 103 | |||
207 | 104 | def setChildClient(self, child_client): | ||
208 | 105 | self.child_client = child_client | ||
209 | 106 | if self._request_buffer is not None: | ||
210 | 107 | self.child_client.sendData(self._request_buffer.getvalue()) | ||
211 | 108 | self._request_buffer = None | ||
212 | 109 | if self._request_data_done: | ||
213 | 110 | self.child_client.endData() | ||
214 | 111 | |||
215 | 112 | def allHeadersReceived(self, command, path, version): | ||
216 | 113 | # Normally done in `requestReceived`, but we disable that since it | ||
217 | 114 | # does other things we don't want. | ||
218 | 115 | self.method, self.uri, self.clientproto = command, path, version | ||
219 | 116 | self.client = self.channel.transport.getPeer() | ||
220 | 117 | self.host = self.channel.transport.getHost() | ||
221 | 118 | |||
222 | 119 | remote_parsed = urlparse(self.channel.factory.remote_url) | ||
223 | 120 | request_parsed = urlparse(path) | ||
224 | 121 | headers = self.getAllHeaders().copy() | ||
225 | 122 | if b"host" not in headers and request_parsed.netloc: | ||
226 | 123 | headers[b"host"] = request_parsed.netloc | ||
227 | 124 | if remote_parsed.username: | ||
228 | 125 | auth = (remote_parsed.username + ":" + | ||
229 | 126 | remote_parsed.password).encode("ASCII") | ||
230 | 127 | authHeader = b"Basic " + base64.b64encode(auth) | ||
231 | 128 | headers[b"proxy-authorization"] = authHeader | ||
232 | 129 | self.client_factory = SnapProxyClientFactory( | ||
233 | 130 | command, path, version, headers, b"", self) | ||
234 | 131 | reactor.connectTCP( | ||
235 | 132 | remote_parsed.hostname, remote_parsed.port, self.client_factory) | ||
236 | 133 | |||
237 | 134 | def requestReceived(self, command, path, version): | ||
238 | 135 | # We do most of our work in `allHeadersReceived` instead. | ||
239 | 136 | pass | ||
240 | 137 | |||
241 | 138 | def rawDataReceived(self, data): | ||
242 | 139 | if self.child_client is not None: | ||
243 | 140 | if not self._request_data_done: | ||
244 | 141 | self.child_client.sendData(data) | ||
245 | 142 | else: | ||
246 | 143 | if self._request_buffer is None: | ||
247 | 144 | self._request_buffer = io.BytesIO() | ||
248 | 145 | self._request_buffer.write(data) | ||
249 | 146 | |||
250 | 147 | def handleEndHeaders(self): | ||
251 | 148 | # Cut-down version of Request.write. We must avoid switching to | ||
252 | 149 | # chunked encoding for the sake of CONNECT; since our actual | ||
253 | 150 | # response data comes from another proxy, we can cut some corners. | ||
254 | 151 | if self.startedWriting: | ||
255 | 152 | return | ||
256 | 153 | self.startedWriting = 1 | ||
257 | 154 | l = [] | ||
258 | 155 | l.append( | ||
259 | 156 | self.clientproto + b" " + intToBytes(self.code) + b" " + | ||
260 | 157 | self.code_message + b"\r\n") | ||
261 | 158 | for name, values in self.responseHeaders.getAllRawHeaders(): | ||
262 | 159 | for value in values: | ||
263 | 160 | l.extend([name, b": ", value, b"\r\n"]) | ||
264 | 161 | l.append(b"\r\n") | ||
265 | 162 | self.transport.writeSequence(l) | ||
266 | 163 | |||
267 | 164 | def write(self, data): | ||
268 | 165 | if self.channel is not None: | ||
269 | 166 | self.channel.resetTimeout() | ||
270 | 167 | http.Request.write(self, data) | ||
271 | 168 | |||
272 | 169 | def endData(self): | ||
273 | 170 | if self.child_client is not None: | ||
274 | 171 | self.child_client.endData() | ||
275 | 172 | self._request_data_done = True | ||
276 | 173 | |||
277 | 174 | |||
278 | 175 | @implementer(IHalfCloseableProtocol) | ||
279 | 176 | class SnapProxy(http.HTTPChannel): | ||
280 | 177 | """A channel that streams request data. | ||
281 | 178 | |||
282 | 179 | The stock HTTPChannel isn't quite suitable for our needs, because it | ||
283 | 180 | expects to read the entire request data before passing control to the | ||
284 | 181 | request. This doesn't work well for CONNECT. | ||
285 | 182 | """ | ||
286 | 183 | |||
287 | 184 | requestFactory = SnapProxyRequest | ||
288 | 185 | |||
289 | 186 | def checkPersistence(self, request, version): | ||
290 | 187 | # ProxyClient.__init__ forces "Connection: close". | ||
291 | 188 | return False | ||
292 | 189 | |||
293 | 190 | def allHeadersReceived(self): | ||
294 | 191 | http.HTTPChannel.allHeadersReceived(self) | ||
295 | 192 | self.requests[-1].allHeadersReceived( | ||
296 | 193 | self._command, self._path, self._version) | ||
297 | 194 | if self._command == b"CONNECT": | ||
298 | 195 | # This is a lie, but we don't want HTTPChannel to decide that | ||
299 | 196 | # the request is finished just because a CONNECT request | ||
300 | 197 | # (naturally) has no Content-Length. | ||
301 | 198 | self.length = -1 | ||
302 | 199 | |||
303 | 200 | def rawDataReceived(self, data): | ||
304 | 201 | self.resetTimeout() | ||
305 | 202 | if self.requests: | ||
306 | 203 | self.requests[-1].rawDataReceived(data) | ||
307 | 204 | |||
308 | 205 | def readConnectionLost(self): | ||
309 | 206 | for request in self.requests: | ||
310 | 207 | request.endData() | ||
311 | 208 | |||
312 | 209 | def writeConnectionLost(self): | ||
313 | 210 | pass | ||
314 | 211 | |||
315 | 212 | |||
316 | 213 | class SnapProxyFactory(http.HTTPFactory): | ||
317 | 214 | |||
318 | 215 | protocol = SnapProxy | ||
319 | 216 | |||
320 | 217 | def __init__(self, manager, remote_url, *args, **kwargs): | ||
321 | 218 | http.HTTPFactory.__init__(self, *args, **kwargs) | ||
322 | 219 | self.manager = manager | ||
323 | 220 | self.remote_url = remote_url | ||
324 | 221 | |||
325 | 222 | def log(self, request): | ||
326 | 223 | # Log requests to the build log rather than to Twisted. | ||
327 | 224 | # Reimplement log formatting because there's no point logging the IP | ||
328 | 225 | # here. | ||
329 | 226 | referrer = http._escape(request.getHeader(b"referer") or b"-") | ||
330 | 227 | agent = http._escape(request.getHeader(b"user-agent") or b"-") | ||
331 | 228 | line = ( | ||
332 | 229 | u'%(timestamp)s "%(method)s %(uri)s %(protocol)s" ' | ||
333 | 230 | u'%(code)d %(length)s "%(referrer)s" "%(agent)s"\n' % { | ||
334 | 231 | 'timestamp': self._logDateTime, | ||
335 | 232 | 'method': http._escape(request.method), | ||
336 | 233 | 'uri': http._escape(request.uri), | ||
337 | 234 | 'protocol': http._escape(request.clientproto), | ||
338 | 235 | 'code': request.code, | ||
339 | 236 | 'length': request.sentLength or "-", | ||
340 | 237 | 'referrer': referrer, | ||
341 | 238 | 'agent': agent, | ||
342 | 239 | }) | ||
343 | 240 | self.manager._slave.log(line.encode("UTF-8")) | ||
344 | 241 | |||
345 | 242 | |||
346 | 24 | class SnapBuildState(DebianBuildState): | 243 | class SnapBuildState(DebianBuildState): |
347 | 25 | BUILD_SNAP = "BUILD_SNAP" | 244 | BUILD_SNAP = "BUILD_SNAP" |
348 | 26 | 245 | ||
349 | @@ -47,9 +266,49 @@ | |||
350 | 47 | self.revocation_endpoint = extra_args.get("revocation_endpoint") | 266 | self.revocation_endpoint = extra_args.get("revocation_endpoint") |
351 | 48 | self.build_source_tarball = extra_args.get( | 267 | self.build_source_tarball = extra_args.get( |
352 | 49 | "build_source_tarball", False) | 268 | "build_source_tarball", False) |
353 | 269 | self.proxy_service = None | ||
354 | 50 | 270 | ||
355 | 51 | super(SnapBuildManager, self).initiate(files, chroot, extra_args) | 271 | super(SnapBuildManager, self).initiate(files, chroot, extra_args) |
356 | 52 | 272 | ||
357 | 273 | def startProxy(self): | ||
358 | 274 | """Start the local snap proxy, if necessary.""" | ||
359 | 275 | if not self.proxy_url: | ||
360 | 276 | return [] | ||
361 | 277 | proxy_port = self._slave._config.get("snapmanager", "proxyport") | ||
362 | 278 | proxy_factory = SnapProxyFactory(self, self.proxy_url, timeout=60) | ||
363 | 279 | self.proxy_service = strports.service(proxy_port, proxy_factory) | ||
364 | 280 | self.proxy_service.setServiceParent(self._slave.service) | ||
365 | 281 | if self.backend_name == "lxd": | ||
366 | 282 | proxy_host = self.backend.ipv4_network.ip | ||
367 | 283 | else: | ||
368 | 284 | proxy_host = "localhost" | ||
369 | 285 | return ["--proxy-url", "http://{}:{}/".format(proxy_host, proxy_port)] | ||
370 | 286 | |||
371 | 287 | def stopProxy(self): | ||
372 | 288 | """Stop the local snap proxy, if necessary.""" | ||
373 | 289 | if self.proxy_service is None: | ||
374 | 290 | return | ||
375 | 291 | self.proxy_service.disownServiceParent() | ||
376 | 292 | self.proxy_service = None | ||
377 | 293 | |||
378 | 294 | def revokeProxyToken(self): | ||
379 | 295 | """Revoke builder proxy token.""" | ||
380 | 296 | if not self.revocation_endpoint: | ||
381 | 297 | return | ||
382 | 298 | self._slave.log("Revoking proxy token...\n") | ||
383 | 299 | url = urlparse(self.proxy_url) | ||
384 | 300 | auth = "{}:{}".format(url.username, url.password) | ||
385 | 301 | headers = { | ||
386 | 302 | "Authorization": "Basic {}".format(base64.b64encode(auth)) | ||
387 | 303 | } | ||
388 | 304 | req = Request(self.revocation_endpoint, None, headers) | ||
389 | 305 | req.get_method = lambda: "DELETE" | ||
390 | 306 | try: | ||
391 | 307 | urlopen(req) | ||
392 | 308 | except (HTTPError, URLError) as e: | ||
393 | 309 | self._slave.log( | ||
394 | 310 | "Unable to revoke token for %s: %s" % (url.username, e)) | ||
395 | 311 | |||
396 | 53 | def status(self): | 312 | def status(self): |
397 | 54 | status_path = get_build_path(self.home, self._buildid, "status") | 313 | status_path = get_build_path(self.home, self._buildid, "status") |
398 | 55 | try: | 314 | try: |
399 | @@ -78,8 +337,7 @@ | |||
400 | 78 | file=sys.stderr) | 337 | file=sys.stderr) |
401 | 79 | if self.build_url: | 338 | if self.build_url: |
402 | 80 | args.extend(["--build-url", self.build_url]) | 339 | args.extend(["--build-url", self.build_url]) |
405 | 81 | if self.proxy_url: | 340 | args.extend(self.startProxy()) |
404 | 82 | args.extend(["--proxy-url", self.proxy_url]) | ||
406 | 83 | if self.revocation_endpoint: | 341 | if self.revocation_endpoint: |
407 | 84 | args.extend(["--revocation-endpoint", self.revocation_endpoint]) | 342 | args.extend(["--revocation-endpoint", self.revocation_endpoint]) |
408 | 85 | if self.branch is not None: | 343 | if self.branch is not None: |
409 | @@ -95,6 +353,8 @@ | |||
410 | 95 | 353 | ||
411 | 96 | def iterate_BUILD_SNAP(self, retcode): | 354 | def iterate_BUILD_SNAP(self, retcode): |
412 | 97 | """Finished building the snap.""" | 355 | """Finished building the snap.""" |
413 | 356 | self.stopProxy() | ||
414 | 357 | self.revokeProxyToken() | ||
415 | 98 | if retcode == RETCODE_SUCCESS: | 358 | if retcode == RETCODE_SUCCESS: |
416 | 99 | self.gatherResults() | 359 | self.gatherResults() |
417 | 100 | print("Returning build status: OK") | 360 | print("Returning build status: OK") |
418 | 101 | 361 | ||
419 | === modified file 'lpbuildd/target/build_snap.py' | |||
420 | --- lpbuildd/target/build_snap.py 2018-06-05 02:06:04 +0000 | |||
421 | +++ lpbuildd/target/build_snap.py 2018-06-10 12:47:59 +0000 | |||
422 | @@ -5,30 +5,11 @@ | |||
423 | 5 | 5 | ||
424 | 6 | __metaclass__ = type | 6 | __metaclass__ = type |
425 | 7 | 7 | ||
426 | 8 | import base64 | ||
427 | 9 | from collections import OrderedDict | 8 | from collections import OrderedDict |
428 | 10 | import json | 9 | import json |
429 | 11 | import logging | 10 | import logging |
430 | 12 | import os.path | 11 | import os.path |
431 | 13 | import sys | 12 | import sys |
432 | 14 | try: | ||
433 | 15 | from urllib.error import ( | ||
434 | 16 | HTTPError, | ||
435 | 17 | URLError, | ||
436 | 18 | ) | ||
437 | 19 | from urllib.request import ( | ||
438 | 20 | Request, | ||
439 | 21 | urlopen, | ||
440 | 22 | ) | ||
441 | 23 | from urllib.parse import urlparse | ||
442 | 24 | except ImportError: | ||
443 | 25 | from urllib2 import ( | ||
444 | 26 | HTTPError, | ||
445 | 27 | Request, | ||
446 | 28 | URLError, | ||
447 | 29 | urlopen, | ||
448 | 30 | ) | ||
449 | 31 | from urlparse import urlparse | ||
450 | 32 | 13 | ||
451 | 33 | from lpbuildd.target.operation import Operation | 14 | from lpbuildd.target.operation import Operation |
452 | 34 | from lpbuildd.target.vcs import VCSOperationMixin | 15 | from lpbuildd.target.vcs import VCSOperationMixin |
453 | @@ -206,21 +187,6 @@ | |||
454 | 206 | cwd=os.path.join("/build", self.args.name), | 187 | cwd=os.path.join("/build", self.args.name), |
455 | 207 | env=env) | 188 | env=env) |
456 | 208 | 189 | ||
457 | 209 | def revoke_token(self): | ||
458 | 210 | """Revoke builder proxy token.""" | ||
459 | 211 | logger.info("Revoking proxy token...") | ||
460 | 212 | url = urlparse(self.args.proxy_url) | ||
461 | 213 | auth = '{}:{}'.format(url.username, url.password) | ||
462 | 214 | headers = { | ||
463 | 215 | 'Authorization': 'Basic {}'.format(base64.b64encode(auth)) | ||
464 | 216 | } | ||
465 | 217 | req = Request(self.args.revocation_endpoint, None, headers) | ||
466 | 218 | req.get_method = lambda: 'DELETE' | ||
467 | 219 | try: | ||
468 | 220 | urlopen(req) | ||
469 | 221 | except (HTTPError, URLError): | ||
470 | 222 | logger.exception('Unable to revoke token for %s', url.username) | ||
471 | 223 | |||
472 | 224 | def run(self): | 190 | def run(self): |
473 | 225 | try: | 191 | try: |
474 | 226 | self.install() | 192 | self.install() |
475 | @@ -234,7 +200,4 @@ | |||
476 | 234 | except Exception: | 200 | except Exception: |
477 | 235 | logger.exception('Build failed') | 201 | logger.exception('Build failed') |
478 | 236 | return RETCODE_FAILURE_BUILD | 202 | return RETCODE_FAILURE_BUILD |
479 | 237 | finally: | ||
480 | 238 | if self.args.revocation_endpoint is not None: | ||
481 | 239 | self.revoke_token() | ||
482 | 240 | return 0 | 203 | return 0 |
483 | 241 | 204 | ||
484 | === modified file 'lpbuildd/tests/test_snap.py' | |||
485 | --- lpbuildd/tests/test_snap.py 2018-04-21 10:04:00 +0000 | |||
486 | +++ lpbuildd/tests/test_snap.py 2018-06-10 12:47:59 +0000 | |||
487 | @@ -10,10 +10,25 @@ | |||
488 | 10 | TempDir, | 10 | TempDir, |
489 | 11 | ) | 11 | ) |
490 | 12 | from testtools import TestCase | 12 | from testtools import TestCase |
491 | 13 | from testtools.content import text_content | ||
492 | 14 | from testtools.deferredruntest import AsynchronousDeferredRunTest | ||
493 | 15 | from twisted.internet import ( | ||
494 | 16 | defer, | ||
495 | 17 | reactor, | ||
496 | 18 | utils, | ||
497 | 19 | ) | ||
498 | 20 | from twisted.web import ( | ||
499 | 21 | http, | ||
500 | 22 | proxy, | ||
501 | 23 | resource, | ||
502 | 24 | server, | ||
503 | 25 | static, | ||
504 | 26 | ) | ||
505 | 13 | 27 | ||
506 | 14 | from lpbuildd.snap import ( | 28 | from lpbuildd.snap import ( |
507 | 15 | SnapBuildManager, | 29 | SnapBuildManager, |
508 | 16 | SnapBuildState, | 30 | SnapBuildState, |
509 | 31 | SnapProxyFactory, | ||
510 | 17 | ) | 32 | ) |
511 | 18 | from lpbuildd.tests.fakeslave import FakeSlave | 33 | from lpbuildd.tests.fakeslave import FakeSlave |
512 | 19 | from lpbuildd.tests.matchers import HasWaitingFiles | 34 | from lpbuildd.tests.matchers import HasWaitingFiles |
513 | @@ -35,6 +50,9 @@ | |||
514 | 35 | 50 | ||
515 | 36 | class TestSnapBuildManagerIteration(TestCase): | 51 | class TestSnapBuildManagerIteration(TestCase): |
516 | 37 | """Run SnapBuildManager through its iteration steps.""" | 52 | """Run SnapBuildManager through its iteration steps.""" |
517 | 53 | |||
518 | 54 | run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5) | ||
519 | 55 | |||
520 | 38 | def setUp(self): | 56 | def setUp(self): |
521 | 39 | super(TestSnapBuildManagerIteration, self).setUp() | 57 | super(TestSnapBuildManagerIteration, self).setUp() |
522 | 40 | self.working_dir = self.useFixture(TempDir()).path | 58 | self.working_dir = self.useFixture(TempDir()).path |
523 | @@ -229,3 +247,64 @@ | |||
524 | 229 | self.assertEqual( | 247 | self.assertEqual( |
525 | 230 | self.buildmanager.iterate, self.buildmanager.iterators[-1]) | 248 | self.buildmanager.iterate, self.buildmanager.iterators[-1]) |
526 | 231 | self.assertFalse(self.slave.wasCalled("buildFail")) | 249 | self.assertFalse(self.slave.wasCalled("buildFail")) |
527 | 250 | |||
528 | 251 | def getListenerURL(self, listener): | ||
529 | 252 | port = listener.getHost().port | ||
530 | 253 | return b"http://localhost:%d/" % port | ||
531 | 254 | |||
532 | 255 | def startFakeRemoteEndpoint(self): | ||
533 | 256 | remote_endpoint = resource.Resource() | ||
534 | 257 | remote_endpoint.putChild("a", static.Data("a" * 1024, "text/plain")) | ||
535 | 258 | remote_endpoint.putChild("b", static.Data("b" * 65536, "text/plain")) | ||
536 | 259 | remote_endpoint_listener = reactor.listenTCP( | ||
537 | 260 | 0, server.Site(remote_endpoint)) | ||
538 | 261 | self.addCleanup(remote_endpoint_listener.stopListening) | ||
539 | 262 | return remote_endpoint_listener | ||
540 | 263 | |||
541 | 264 | def startFakeRemoteProxy(self): | ||
542 | 265 | remote_proxy_factory = http.HTTPFactory() | ||
543 | 266 | remote_proxy_factory.protocol = proxy.Proxy | ||
544 | 267 | remote_proxy_listener = reactor.listenTCP(0, remote_proxy_factory) | ||
545 | 268 | self.addCleanup(remote_proxy_listener.stopListening) | ||
546 | 269 | return remote_proxy_listener | ||
547 | 270 | |||
548 | 271 | def startLocalProxy(self, remote_url): | ||
549 | 272 | proxy_factory = SnapProxyFactory( | ||
550 | 273 | self.buildmanager, remote_url, timeout=60) | ||
551 | 274 | proxy_listener = reactor.listenTCP(0, proxy_factory) | ||
552 | 275 | self.addCleanup(proxy_listener.stopListening) | ||
553 | 276 | return proxy_listener | ||
554 | 277 | |||
555 | 278 | @defer.inlineCallbacks | ||
556 | 279 | def assertCommandSuccess(self, command, extra_env=None): | ||
557 | 280 | env = os.environ | ||
558 | 281 | if extra_env is not None: | ||
559 | 282 | env.update(extra_env) | ||
560 | 283 | out, err, code = yield utils.getProcessOutputAndValue( | ||
561 | 284 | command[0], command[1:], env=env, path=".") | ||
562 | 285 | if code != 0: | ||
563 | 286 | self.addDetail("stdout", text_content(out)) | ||
564 | 287 | self.addDetail("stderr", text_content(err)) | ||
565 | 288 | self.assertEqual(0, code) | ||
566 | 289 | defer.returnValue(out) | ||
567 | 290 | |||
568 | 291 | @defer.inlineCallbacks | ||
569 | 292 | def test_fetch_via_proxy(self): | ||
570 | 293 | remote_endpoint_listener = self.startFakeRemoteEndpoint() | ||
571 | 294 | remote_endpoint_url = self.getListenerURL(remote_endpoint_listener) | ||
572 | 295 | remote_proxy_listener = self.startFakeRemoteProxy() | ||
573 | 296 | proxy_listener = self.startLocalProxy( | ||
574 | 297 | self.getListenerURL(remote_proxy_listener)) | ||
575 | 298 | out = yield self.assertCommandSuccess( | ||
576 | 299 | [b"curl", remote_endpoint_url + b"a"], | ||
577 | 300 | extra_env={b"http_proxy": self.getListenerURL(proxy_listener)}) | ||
578 | 301 | self.assertEqual("a" * 1024, out) | ||
579 | 302 | out = yield self.assertCommandSuccess( | ||
580 | 303 | [b"curl", remote_endpoint_url + b"b"], | ||
581 | 304 | extra_env={b"http_proxy": self.getListenerURL(proxy_listener)}) | ||
582 | 305 | self.assertEqual("b" * 65536, out) | ||
583 | 306 | |||
584 | 307 | # XXX cjwatson 2017-04-13: We should really test the HTTPS case as well, | ||
585 | 308 | # but it's hard to see how to test that in a way that's independent of | ||
586 | 309 | # the code under test since the stock twisted.web.proxy doesn't support | ||
587 | 310 | # CONNECT. | ||
588 | 232 | 311 | ||
589 | === modified file 'template-buildd-slave.conf' | |||
590 | --- template-buildd-slave.conf 2015-05-11 06:09:19 +0000 | |||
591 | +++ template-buildd-slave.conf 2018-06-10 12:47:59 +0000 | |||
592 | @@ -12,3 +12,6 @@ | |||
593 | 12 | 12 | ||
594 | 13 | [translationtemplatesmanager] | 13 | [translationtemplatesmanager] |
595 | 14 | resultarchive = translation-templates.tar.gz | 14 | resultarchive = translation-templates.tar.gz |
596 | 15 | |||
597 | 16 | [snapmanager] | ||
598 | 17 | proxyport = @SNAPPROXYPORT@ |
See also https:/ /bugs.launchpad .net/snapcraft/ +bug/1682534.