Merge ~ddstreet/software-properties:lp645404 into software-properties:ubuntu/master
- Git
- lp:~ddstreet/software-properties
- lp645404
- Merge into ubuntu/master
Status: | Rejected | ||||
---|---|---|---|---|---|
Rejected by: | Dan Streetman | ||||
Proposed branch: | ~ddstreet/software-properties:lp645404 | ||||
Merge into: | software-properties:ubuntu/master | ||||
Diff against target: |
3367 lines (+1764/-1064) 22 files modified
add-apt-repository (+212/-175) debian/control (+2/-0) debian/manpages/add-apt-repository.1 (+107/-34) debian/tests/add-apt-repository-archive (+56/-0) debian/tests/add-apt-repository-cloud (+59/-0) debian/tests/add-apt-repository-ppa (+71/-0) debian/tests/control (+10/-2) dev/null (+0/-167) softwareproperties/SoftwareProperties.py (+17/-35) softwareproperties/cloudarchive.py (+100/-84) softwareproperties/gtk/DialogCacheOutdated.py (+1/-2) softwareproperties/gtk/SoftwarePropertiesGtk.py (+1/-2) softwareproperties/ppa.py (+147/-427) softwareproperties/shortcuthandler.py (+617/-0) softwareproperties/shortcuts.py (+19/-29) softwareproperties/sourceslist.py (+56/-0) softwareproperties/uri.py (+36/-0) tests/test_aptsources.py (+1/-1) tests/test_dbus.py (+45/-66) tests/test_pyflakes.py (+0/-1) tests/test_shortcuts.py (+206/-38) tests/test_sp.py (+1/-1) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James Page | Pending | ||
Corey Bryant | Pending | ||
Julian Andres Klode | Pending | ||
Ubuntu Core Development Team | Pending | ||
Review via email: mp+367276@code.launchpad.net |
Commit message
Description of the change
Dan Streetman (ddstreet) wrote : | # |
Dan Streetman (ddstreet) wrote : | # |
I think this is ready for at least initial review. Note this does not include --remote support.
Julian Andres Klode (juliank) wrote : | # |
I don't like this commit, it makes it harder to read for basically no benefit:
move 'datadir' into kwargs, set default in SoftwareProperties
same for rootdir moving into kwargs in the next commit. kwargs should be avoided as much as humanely possible. It prevents a lot of static checks (especially type checks get harder, and I'd love to add some mypy annotations at some point).
The removal of the Options stuff I don't like very much, I felt it was cleaner before.
Refactoring of ShortCutHandler I can't really follow, it's too big.
Dan Streetman (ddstreet) wrote : | # |
> I don't like this commit, it makes it harder to read for basically no benefit:
> move 'datadir' into kwargs, set default in SoftwareProperties
>
> same for rootdir moving into kwargs in the next commit. kwargs should be
> avoided as much as humanely possible. It prevents a lot of static checks
> (especially type checks get harder, and I'd love to add some mypy annotations
> at some point).
>
> The removal of the Options stuff I don't like very much, I felt it was cleaner
> before.
>
>
> Refactoring of ShortCutHandler I can't really follow, it's too big.
Here's a different approach that makes the absolute minimum changes to SoftwareProperties, and splits up much of the rewriting to possibly be easier for you to review.
Just an initial draft, I need to go over the commits again and check the test updates, so it's not ready to merge yet but shoudl be ready for general review.
Dan Streetman (ddstreet) wrote : | # |
Hold that review for a bit; I'm still working on getting SoftwareProperties out of add-apt-repository.
Dan Streetman (ddstreet) wrote : | # |
Ready for review please.
Dan Streetman (ddstreet) wrote : | # |
gave up on this MR.
moved devel work into
https:/
https:/
There was an error fetching revisions from git servers. Please try again in a few minutes. If the problem persists, contact Launchpad support.
Preview Diff
1 | diff --git a/add-apt-repository b/add-apt-repository | |||
2 | index ea5f7dc..0b2109f 100755 | |||
3 | --- a/add-apt-repository | |||
4 | +++ b/add-apt-repository | |||
5 | @@ -7,190 +7,227 @@ import os | |||
6 | 7 | import sys | 7 | import sys |
7 | 8 | import gettext | 8 | import gettext |
8 | 9 | import locale | 9 | import locale |
15 | 10 | 10 | import argparse | |
16 | 11 | from softwareproperties.SoftwareProperties import SoftwareProperties, shortcut_handler | 11 | import subprocess |
17 | 12 | from softwareproperties.shortcuts import ShortcutException | 12 | |
18 | 13 | import aptsources | 13 | from softwareproperties.shortcuthandler import ShortcutException |
19 | 14 | from aptsources.sourceslist import SourceEntry | 14 | from softwareproperties.shortcuts import shortcut_handler |
20 | 15 | from optparse import OptionParser | 15 | from softwareproperties.ppa import PPAShortcutHandler |
21 | 16 | from softwareproperties.cloudarchive import CloudArchiveShortcutHandler | ||
22 | 17 | from softwareproperties.sourceslist import SourcesListShortcutHandler | ||
23 | 18 | from softwareproperties.uri import URIShortcutHandler | ||
24 | 19 | |||
25 | 20 | from aptsources.distro import get_distro | ||
26 | 21 | from aptsources.sourceslist import (SourcesList, SourceEntry) | ||
27 | 16 | from gettext import gettext as _ | 22 | from gettext import gettext as _ |
28 | 17 | 23 | ||
168 | 18 | if __name__ == "__main__": | 24 | |
169 | 19 | # Force encoding to UTF-8 even in non-UTF-8 locales. | 25 | class AddAptRepository(object): |
170 | 20 | sys.stdout = io.TextIOWrapper( | 26 | def __init__(self): |
171 | 21 | sys.stdout.detach(), encoding="UTF-8", line_buffering=True) | 27 | gettext.textdomain("software-properties") |
172 | 22 | 28 | self.distro = get_distro() | |
173 | 23 | try: | 29 | self.sourceslist = SourcesList() |
174 | 24 | locale.setlocale(locale.LC_ALL, "") | 30 | self.distro.get_sources(self.sourceslist) |
175 | 25 | except: | 31 | |
176 | 26 | pass | 32 | def parse_args(self, args): |
177 | 27 | gettext.textdomain("software-properties") | 33 | description = "Only ONE of -p, -c, -a, or old-style 'line' can be specified" |
178 | 28 | usage = """Usage: %prog <sourceline> | 34 | |
179 | 29 | 35 | parser = argparse.ArgumentParser(description=description) | |
180 | 30 | %prog is a script for adding apt sources.list entries. | 36 | parser.add_argument("-d", "--debug", action="store_true", |
181 | 31 | It can be used to add any repository and also provides a shorthand | 37 | help=_("Print debug")) |
182 | 32 | syntax for adding a Launchpad PPA (Personal Package Archive) | 38 | parser.add_argument("-r", "--remove", action="store_true", |
183 | 33 | repository. | 39 | help=_("Disable repository")) |
184 | 34 | 40 | parser.add_argument("-s", "--enable-source", action="store_true", | |
185 | 35 | <sourceline> - The apt repository source line to add. This is one of: | 41 | help=_("Allow downloading of the source packages from the repository")) |
186 | 36 | a complete apt line in quotes, | 42 | parser.add_argument("-C", "--component", action="append", default=[], |
187 | 37 | a repo url and areas in quotes (areas defaults to 'main') | 43 | help=_("Components to use with the repository")) |
188 | 38 | a PPA shortcut. | 44 | parser.add_argument("-y", "--yes", action="store_true", |
189 | 39 | a distro component | 45 | help=_("Assume yes to all queries")) |
190 | 40 | 46 | parser.add_argument("-n", "--no-update", dest="update", action="store_false", | |
191 | 41 | Examples: | 47 | help=_("Do not update package cache after adding")) |
192 | 42 | apt-add-repository 'deb http://myserver/path/to/repo stable myrepo' | 48 | parser.add_argument("-u", "--update", action="store_true", default=True, |
193 | 43 | apt-add-repository 'http://myserver/path/to/repo myrepo' | 49 | help=argparse.SUPPRESS) |
194 | 44 | apt-add-repository 'https://packages.medibuntu.org free non-free' | 50 | parser.add_argument("-l", "--login", action="store_true", |
195 | 45 | apt-add-repository http://extras.ubuntu.com/ubuntu | 51 | help=_("Login to Launchpad.")) |
196 | 46 | apt-add-repository ppa:user/repository | 52 | parser.add_argument("--dry-run", action="store_true", |
197 | 47 | apt-add-repository ppa:user/distro/repository | 53 | help=_("Don't actually make any changes.")) |
198 | 48 | apt-add-repository multiverse | 54 | |
199 | 49 | 55 | group = parser.add_mutually_exclusive_group() | |
200 | 50 | If --remove is given the tool will remove the given sourceline from your | 56 | group.add_argument("-p", "--ppa", |
201 | 51 | sources.list | 57 | help=_("PPA to add")) |
202 | 52 | """ | 58 | group.add_argument("-c", "--cloud", |
203 | 53 | parser = OptionParser(usage) | 59 | help=_("Cloud Archive to add")) |
204 | 54 | # FIXME: provide a --sources-list-file= option that | 60 | group.add_argument("-U", "--uri", |
205 | 55 | # puts the line into a specific file in sources.list.d | 61 | help=_("Archive URI to add")) |
206 | 56 | parser.add_option ("-m", "--massive-debug", action="store_true", | 62 | group.add_argument("-S", "--sourceslist", |
207 | 57 | dest="massive_debug", default=False, | 63 | help=_("Full sources.list entry line to add")) |
208 | 58 | help=_("Print a lot of debug information to the command line")) | 64 | group.add_argument("line", nargs='*', default=[], |
209 | 59 | parser.add_option("-r", "--remove", action="store_true", | 65 | help=_("sources.list line to add (deprecated)")) |
210 | 60 | dest="remove", default=False, | 66 | |
211 | 61 | help=_("remove repository from sources.list.d directory")) | 67 | self.parser = parser |
212 | 62 | parser.add_option("-s", "--enable-source", action="store_true", | 68 | self.options = self.parser.parse_args(args) |
213 | 63 | dest="enable_source", default=False, | 69 | |
214 | 64 | help=_("Allow downloading of the source packages from the repository")) | 70 | @property |
215 | 65 | parser.add_option("-y", "--yes", action="store_true", | 71 | def dry_run(self): |
216 | 66 | dest="assume_yes", default=False, | 72 | return self.options.dry_run |
217 | 67 | help=_("Assume yes to all queries")) | 73 | |
218 | 68 | parser.add_option("-n", "--no-update", action="store_false", | 74 | @property |
219 | 69 | dest="update", default=True, | 75 | def enable_source(self): |
220 | 70 | help=_("Do not update package cache after adding")) | 76 | return self.options.enable_source |
221 | 71 | parser.add_option("-u", "--update", action="store_true", | 77 | |
222 | 72 | dest="update", default=True, | 78 | @property |
223 | 73 | help=_("Update package cache after adding (legacy option)")) | 79 | def components(self): |
224 | 74 | parser.add_option("-k", "--keyserver", | 80 | return self.options.component |
225 | 75 | dest="keyserver", default="", | 81 | |
226 | 76 | help=_("Legacy option, unused.")) | 82 | def is_components(self, comps): |
227 | 77 | 83 | if not comps: | |
228 | 78 | (options, args) = parser.parse_args() | 84 | return False |
229 | 79 | 85 | return set(comps.split()) <= set([comp.name for comp in self.distro.source_template.components]) | |
230 | 80 | # We prefer to run apt-get update here. The built-in update support | 86 | |
231 | 81 | # does not have any progress, and only works for shortcuts. Moving | 87 | def apt_update(self): |
232 | 82 | # it to something like save() and using apt.progress.text would | 88 | if self.options.update and not self.dry_run: |
233 | 83 | # solve the problem, but the new errors might cause problems with | 89 | # We prefer to run apt-get update here. The built-in update support |
234 | 84 | # the dbus server or other users of the API. Also, it's unclear | 90 | # does not have any progress, and only works for shortcuts. Moving |
235 | 85 | # how good the text progress is or how to pass it best. | 91 | # it to something like save() and using apt.progress.text would |
236 | 86 | update = options.update | 92 | # solve the problem, but the new errors might cause problems with |
237 | 87 | options.update = False | 93 | # the dbus server or other users of the API. Also, it's unclear |
238 | 88 | 94 | # how good the text progress is or how to pass it best. | |
239 | 89 | if os.geteuid() != 0: | 95 | subprocess.run(['apt-get', 'update']) |
240 | 90 | print(_("Error: must run as root")) | 96 | |
241 | 91 | sys.exit(1) | 97 | def prompt_user(self): |
242 | 92 | 98 | if self.dry_run: | |
243 | 93 | if len(args) == 0: | 99 | print(_("DRY-RUN mode: no modifications will be made")) |
244 | 94 | print(_("Error: need a repository as argument")) | 100 | return |
245 | 95 | sys.exit(1) | 101 | if not self.options.yes and sys.stdin.isatty() and not "FORCE_ADD_APT_REPOSITORY" in os.environ: |
107 | 96 | elif len(args) > 1: | ||
108 | 97 | print(_("Error: need a single repository as argument")) | ||
109 | 98 | sys.exit(1) | ||
110 | 99 | |||
111 | 100 | # force new ppa file to be 644 (LP: #399709) | ||
112 | 101 | os.umask(0o022) | ||
113 | 102 | |||
114 | 103 | # get the line | ||
115 | 104 | line = args[0] | ||
116 | 105 | |||
117 | 106 | # add it | ||
118 | 107 | sp = SoftwareProperties(options=options) | ||
119 | 108 | distro = aptsources.distro.get_distro() | ||
120 | 109 | distro.get_sources(sp.sourceslist) | ||
121 | 110 | |||
122 | 111 | # check if its a component that should be added/removed | ||
123 | 112 | components = [comp.name for comp in distro.source_template.components] | ||
124 | 113 | if line in components: | ||
125 | 114 | if options.remove: | ||
126 | 115 | if line in distro.enabled_comps: | ||
127 | 116 | distro.disable_component(line) | ||
128 | 117 | print(_("'%s' distribution component disabled for all sources.") % line) | ||
129 | 118 | else: | ||
130 | 119 | print(_("'%s' distribution component is already disabled for all sources.") % line) | ||
131 | 120 | sys.exit(0) | ||
132 | 121 | else: | ||
133 | 122 | if line not in distro.enabled_comps: | ||
134 | 123 | distro.enable_component(line) | ||
135 | 124 | print(_("'%s' distribution component enabled for all sources.") % line) | ||
136 | 125 | else: | ||
137 | 126 | print(_("'%s' distribution component is already enabled for all sources.") % line) | ||
138 | 127 | sys.exit(0) | ||
139 | 128 | sp.sourceslist.save() | ||
140 | 129 | if update and not options.remove: | ||
141 | 130 | os.execvp("apt-get", ["apt-get", "update"]) | ||
142 | 131 | sys.exit(0) | ||
143 | 132 | |||
144 | 133 | # this wasn't a component name ('multiverse', 'backports'), so its either | ||
145 | 134 | # a actual line to be added or a shortcut. | ||
146 | 135 | try: | ||
147 | 136 | shortcut = shortcut_handler(line) | ||
148 | 137 | except ShortcutException as e: | ||
149 | 138 | print(e) | ||
150 | 139 | sys.exit(1) | ||
151 | 140 | |||
152 | 141 | # display more information about the shortcut / ppa info | ||
153 | 142 | if not options.assume_yes and shortcut.should_confirm(): | ||
154 | 143 | try: | ||
155 | 144 | info = shortcut.info() | ||
156 | 145 | except ShortcutException as e: | ||
157 | 146 | print(e) | ||
158 | 147 | sys.exit(1) | ||
159 | 148 | |||
160 | 149 | print(" %s" % (info["description"] or "")) | ||
161 | 150 | print(_(" More info: %s") % str(info["web_link"])) | ||
162 | 151 | if (sys.stdin.isatty() and | ||
163 | 152 | not "FORCE_ADD_APT_REPOSITORY" in os.environ): | ||
164 | 153 | if options.remove: | ||
165 | 154 | print(_("Press [ENTER] to continue or Ctrl-c to cancel removing it.")) | ||
166 | 155 | else: | ||
167 | 156 | print(_("Press [ENTER] to continue or Ctrl-c to cancel adding it.")) | ||
246 | 157 | try: | 102 | try: |
248 | 158 | sys.stdin.readline() | 103 | input(_("Press [ENTER] to continue or Ctrl-c to cancel.")) |
249 | 159 | except KeyboardInterrupt: | 104 | except KeyboardInterrupt: |
251 | 160 | print("\n") | 105 | print(_("Aborted.")) |
252 | 161 | sys.exit(1) | 106 | sys.exit(1) |
253 | 162 | 107 | ||
254 | 108 | def prompt_user_shortcut(self, shortcut): | ||
255 | 109 | '''Display more information about the shortcut / ppa info''' | ||
256 | 110 | print(_("Repository: '%s'") % shortcut.SourceEntry().line) | ||
257 | 111 | if shortcut.description: | ||
258 | 112 | print(_("Description:")) | ||
259 | 113 | print(shortcut.description) | ||
260 | 114 | if shortcut.web_link: | ||
261 | 115 | print(_("More info: %s") % shortcut.web_link) | ||
262 | 116 | if self.options.remove: | ||
263 | 117 | print(_("Removing repository.")) | ||
264 | 118 | else: | ||
265 | 119 | print(_("Adding repository.")) | ||
266 | 120 | self.prompt_user() | ||
267 | 121 | |||
268 | 122 | def change_components(self): | ||
269 | 123 | for c in self.components: | ||
270 | 124 | if self.options.remove: | ||
271 | 125 | self.distro.disable_component(c) | ||
272 | 126 | print(_("Removed component %s") % c) | ||
273 | 127 | else: | ||
274 | 128 | self.distro.enable_component(c) | ||
275 | 129 | print(_("Added component %s") % c) | ||
276 | 130 | if not self.dry_run: | ||
277 | 131 | self.sourceslist.save() | ||
278 | 132 | |||
279 | 133 | def change_source(self): | ||
280 | 134 | newlist = [] | ||
281 | 135 | for s in [s for s in self.sourceslist if not s.invalid and not s.disabled]: | ||
282 | 136 | if self.options.remove and s.type == self.distro.source_type: | ||
283 | 137 | s.set_enabled(False) | ||
284 | 138 | print(_("Disabled: %s") % s.str()) | ||
285 | 139 | elif not self.options.remove and s.type == self.distro.binary_type: | ||
286 | 140 | s = SourceEntry(str(s)) | ||
287 | 141 | s.type = self.distro.source_type | ||
288 | 142 | self.sourceslist.add(s.type, s.uri, s.dist, s.comps, comment=s.comment, | ||
289 | 143 | file=s.file, architectures=s.architectures) | ||
290 | 144 | print(_("Enabled: %s") % s.str()) | ||
291 | 145 | if not self.dry_run: | ||
292 | 146 | self.sourceslist.save() | ||
293 | 147 | |||
294 | 148 | def global_change(self): | ||
295 | 149 | if self.components: | ||
296 | 150 | if self.options.remove: | ||
297 | 151 | print(_("Removing component(s) '%s' from all repositories.") % ', '.join(self.components)) | ||
298 | 152 | else: | ||
299 | 153 | print(_("Adding component(s) '%s' to all repositories.") % ', '.join(self.components)) | ||
300 | 154 | if self.enable_source: | ||
301 | 155 | if self.options.remove: | ||
302 | 156 | print(_("Disabling %s for all repositories.") % self.distro.source_type) | ||
303 | 157 | else: | ||
304 | 158 | print(_("Enabling %s for all repositories.") % self.distro.source_type) | ||
305 | 159 | self.prompt_user() | ||
306 | 160 | if self.components: | ||
307 | 161 | self.change_components() | ||
308 | 162 | if self.enable_source: | ||
309 | 163 | self.change_source() | ||
310 | 164 | |||
311 | 165 | def main(self, args=sys.argv[1:]): | ||
312 | 166 | self.parse_args(args) | ||
313 | 167 | |||
314 | 168 | if not self.dry_run and os.geteuid() != 0: | ||
315 | 169 | print(_("Error: must run as root")) | ||
316 | 170 | return False | ||
317 | 171 | |||
318 | 172 | line = ' '.join(self.options.line) | ||
319 | 173 | if line == '-': | ||
320 | 174 | line = sys.stdin.readline().strip() | ||
321 | 175 | |||
322 | 176 | # if 'line' is only (valid) components, handle as if only -C was used with no line | ||
323 | 177 | if self.is_components(line): | ||
324 | 178 | self.options.component += line.split() | ||
325 | 179 | line = '' | ||
326 | 180 | |||
327 | 181 | if self.options.ppa: | ||
328 | 182 | source = self.options.ppa | ||
329 | 183 | if not ':' in source: | ||
330 | 184 | source = 'ppa:' + source | ||
331 | 185 | handler = PPAShortcutHandler | ||
332 | 186 | elif self.options.cloud: | ||
333 | 187 | source = self.options.cloud | ||
334 | 188 | if not ':' in source: | ||
335 | 189 | source = 'uca:' + source | ||
336 | 190 | handler = CloudArchiveShortcutHandler | ||
337 | 191 | elif self.options.uri: | ||
338 | 192 | source = self.options.uri | ||
339 | 193 | handler = URIShortcutHandler | ||
340 | 194 | elif self.options.sourceslist: | ||
341 | 195 | source = self.options.sourceslist | ||
342 | 196 | handler = SourcesListShortcutHandler | ||
343 | 197 | elif line: | ||
344 | 198 | source = line | ||
345 | 199 | handler = shortcut_handler | ||
346 | 200 | elif self.enable_source or self.components: | ||
347 | 201 | self.global_change() | ||
348 | 202 | self.apt_update() | ||
349 | 203 | return True | ||
350 | 204 | else: | ||
351 | 205 | print(_("Error: no actions requested.")) | ||
352 | 206 | self.parser.print_help() | ||
353 | 207 | return False | ||
354 | 163 | 208 | ||
355 | 164 | if options.remove: | ||
356 | 165 | try: | 209 | try: |
359 | 166 | (line, file) = shortcut.expand( | 210 | shortcut_params = { |
360 | 167 | sp.distro.codename, sp.distro.id.lower()) | 211 | 'login': self.options.login, |
361 | 212 | 'enable_source': self.enable_source, | ||
362 | 213 | 'dry_run': self.dry_run, | ||
363 | 214 | 'components': self.components, | ||
364 | 215 | } | ||
365 | 216 | shortcut = handler(source, **shortcut_params) | ||
366 | 168 | except ShortcutException as e: | 217 | except ShortcutException as e: |
367 | 169 | print(e) | 218 | print(e) |
381 | 170 | sys.exit(1) | 219 | return False |
369 | 171 | deb_line = sp.expand_http_line(line) | ||
370 | 172 | debsrc_line = 'deb-src' + deb_line[3:] | ||
371 | 173 | deb_entry = SourceEntry(deb_line, file) | ||
372 | 174 | debsrc_entry = SourceEntry(debsrc_line, file) | ||
373 | 175 | try: | ||
374 | 176 | sp.remove_source(deb_entry) | ||
375 | 177 | except ValueError: | ||
376 | 178 | print(_("Error: '%s' doesn't exist in a sourcelist file") % deb_line) | ||
377 | 179 | try: | ||
378 | 180 | sp.remove_source(debsrc_entry) | ||
379 | 181 | except ValueError: | ||
380 | 182 | print(_("Error: '%s' doesn't exist in a sourcelist file") % debsrc_line) | ||
382 | 183 | 220 | ||
391 | 184 | else: | 221 | self.prompt_user_shortcut(shortcut) |
392 | 185 | try: | 222 | |
393 | 186 | if not sp.add_source_from_shortcut(shortcut, options.enable_source): | 223 | if self.options.remove: |
394 | 187 | print(_("Error: '%s' invalid") % line) | 224 | shortcut.remove() |
395 | 188 | sys.exit(1) | 225 | else: |
396 | 189 | except ShortcutException as e: | 226 | shortcut.add() |
397 | 190 | print(e) | 227 | |
398 | 191 | sys.exit(1) | 228 | self.apt_update() |
399 | 229 | return True | ||
400 | 192 | 230 | ||
405 | 193 | sp.sourceslist.save() | 231 | if __name__ == '__main__': |
406 | 194 | if update and not options.remove: | 232 | addaptrepo = AddAptRepository() |
407 | 195 | os.execvp("apt-get", ["apt-get", "update"]) | 233 | sys.exit(0 if addaptrepo.main() else 1) |
404 | 196 | sys.exit(0) | ||
408 | diff --git a/debian/control b/debian/control | |||
409 | index 6998d5e..9f9441f 100644 | |||
410 | --- a/debian/control | |||
411 | +++ b/debian/control | |||
412 | @@ -21,6 +21,7 @@ Build-Depends: dbus-x11 <!nocheck>, | |||
413 | 21 | python3-distro-info <!nocheck>, | 21 | python3-distro-info <!nocheck>, |
414 | 22 | python3-distutils-extra, | 22 | python3-distutils-extra, |
415 | 23 | python3-gi <!nocheck>, | 23 | python3-gi <!nocheck>, |
416 | 24 | python3-launchpadlib, | ||
417 | 24 | python3-mock <!nocheck>, | 25 | python3-mock <!nocheck>, |
418 | 25 | python3-requests-unixsocket <!nocheck>, | 26 | python3-requests-unixsocket <!nocheck>, |
419 | 26 | python3-setuptools, | 27 | python3-setuptools, |
420 | @@ -40,6 +41,7 @@ Depends: gpg, | |||
421 | 40 | python3-apt (>= | 41 | python3-apt (>= |
422 | 41 | 0.6.20ubuntu16), | 42 | 0.6.20ubuntu16), |
423 | 42 | python3-gi, | 43 | python3-gi, |
424 | 44 | python3-launchpadlib, | ||
425 | 43 | ${misc:Depends}, | 45 | ${misc:Depends}, |
426 | 44 | ${python3:Depends} | 46 | ${python3:Depends} |
427 | 45 | Recommends: unattended-upgrades | 47 | Recommends: unattended-upgrades |
428 | diff --git a/debian/manpages/add-apt-repository.1 b/debian/manpages/add-apt-repository.1 | |||
429 | index 3a195a4..93d7e94 100644 | |||
430 | --- a/debian/manpages/add-apt-repository.1 | |||
431 | +++ b/debian/manpages/add-apt-repository.1 | |||
432 | @@ -4,53 +4,126 @@ add-apt-repository \- Adds a repository into the | |||
433 | 4 | /etc/apt/sources.list or /etc/apt/sources.list.d | 4 | /etc/apt/sources.list or /etc/apt/sources.list.d |
434 | 5 | or removes an existing one | 5 | or removes an existing one |
435 | 6 | .SH SYNOPSIS | 6 | .SH SYNOPSIS |
437 | 7 | .B add-apt-repository \fI[OPTIONS]\fR \fIREPOSITORY\fR | 7 | .B add-apt-repository \fI[OPTIONS]\fR \fI[LINE]\fR |
438 | 8 | .SH DESCRIPTION | 8 | .SH DESCRIPTION |
439 | 9 | .B add-apt-repository | 9 | .B add-apt-repository |
440 | 10 | is a script which adds an external APT repository to either | 10 | is a script which adds an external APT repository to either |
441 | 11 | /etc/apt/sources.list or a file in /etc/apt/sources.list.d/ | 11 | /etc/apt/sources.list or a file in /etc/apt/sources.list.d/ |
442 | 12 | or removes an already existing repository. | 12 | or removes an already existing repository. |
443 | 13 | 13 | ||
446 | 14 | The options supported by add-apt-repository are: | 14 | .SH OPTIONS |
447 | 15 | 15 | Note that the \fB--ppa\fR, \fB--cloud\fR, \fB--archive\fR, and \fBLINE\fR parameters are mutually exclusive; only one (or none) of them may be specified. | |
448 | 16 | .TP | ||
449 | 16 | .B -h, --help | 17 | .B -h, --help |
450 | 17 | Show help message and exit | 18 | Show help message and exit |
455 | 18 | 19 | .TP | |
456 | 19 | .B -m, --massive-debug | 20 | .B -d, --debug |
457 | 20 | Print a lot of debug information to the command line | 21 | Print debug information to the command line |
458 | 21 | 22 | .TP | |
459 | 22 | .B -r, --remove | 23 | .B -r, --remove |
461 | 23 | Remove the specified repository | 24 | Remove the specified repository; this first will disable (comment out) the matching line(s), |
462 | 25 | and then any modified file(s) under sources.list.d/ will be removed if they contain only empty and commented lines. | ||
463 | 24 | 26 | ||
464 | 27 | Note that this performs differently when used with the \fI--enable-source\fR and/or \fI--component\fR parameters. | ||
465 | 28 | Without either of those parameters, this removes the specified repository, including any \fBdeb-src\fR line(s), and all components. | ||
466 | 29 | If \fI--enable-source\fR is used, this removes \fBonly\fR the 'deb-src' line(s). | ||
467 | 30 | If \fI--component\fR is used, this removes \fBonly\FR the specified component(s), and only removes the repository if no components remain. | ||
468 | 31 | |||
469 | 32 | If both \fI--enable-source\fR and \fI--component\fR are used with \fI--remove\fR, the actions are performed separately: the specified | ||
470 | 33 | component(s) will be removed from both \fBdeb\fR and \fBdeb-src\fR lines, and \fBdeb-src\fR lines will be disabled. | ||
471 | 34 | .TP | ||
472 | 25 | .B -y, --yes | 35 | .B -y, --yes |
473 | 26 | Assume yes to all queries | 36 | Assume yes to all queries |
481 | 27 | 37 | .TP | |
482 | 28 | .B -u, --update | 38 | .B -n, --no-update |
483 | 29 | After adding the repository, update the package cache with packages from this repository (avoids need to apt-get update) | 39 | After adding the repository, do not update the package cache |
484 | 30 | 40 | .TP | |
485 | 31 | .B -k, --keyserver | 41 | .B -l, --login |
486 | 32 | Use a custom keyserver URL instead of the default | 42 | Login to Launchpad (this is only needed for private PPAs) |
487 | 33 | 43 | .TP | |
488 | 34 | .B -s, --enable-source | 44 | .B -s, --enable-source |
508 | 35 | Allow downloading of the source packages from the repository | 45 | Allow downloading of the source packages from the repository; specifically, this adds and enables a 'deb-src' line for the repostiory. |
509 | 36 | 46 | If this parameter is used without any repository, it will enable source for all currently existing repositories. | |
510 | 37 | 47 | .TP | |
511 | 38 | .SH REPOSITORY STRING | 48 | .B -C, --component |
512 | 39 | \fIREPOSITORY\fR can be either a line that can be added directly to | 49 | Which component(s) should be used with the specified repository. If not specified, this will default to 'main'. |
513 | 40 | sources.list(5), in the form ppa:<user>/<ppa-name> for adding Personal | 50 | This may be used multiple times to specify multiple components. If this is used without any repository, it will add the |
514 | 41 | Package Archives, or a distribution component to enable. | 51 | component(s) to all currently existing repositories. |
515 | 42 | 52 | .TP | |
516 | 43 | In the first form, \fIREPOSITORY\fR will just be appended to | 53 | .B --dry-run |
517 | 44 | /etc/apt/sources.list. | 54 | Show what would be done, but don't make any changes |
518 | 45 | 55 | .TP | |
519 | 46 | In the second form, ppa:<user>/<ppa-name> will be expanded to the full deb line | 56 | .B -p, --ppa |
520 | 47 | of the PPA and added into a new file in the /etc/apt/sources.list.d/ | 57 | Add an Ubuntu Launchpad Personal Package Archive. |
521 | 48 | directory. | 58 | Must be in the format \fBppa:USER/PPA\fR, \fBUSER/PPA\fR, or \fBUSER\fR. |
522 | 49 | The GPG public key of the newly added PPA will also be downloaded and | 59 | The \fBUSER\fR parameter should be the Launchpad team or person that owns the PPA. |
523 | 50 | added to apt's keyring. | 60 | The \fBPPA\fR parameter should be the name of the PPA; if not provided, it defaults to 'ppa'. |
524 | 51 | 61 | The GPG public key of the PPA will also be downloaded and added to apt's keyring. | |
525 | 52 | In the third form, the given distribution component will be enabled for all | 62 | To add a private PPA, you must also use the \FI--login\fR parameter, and of course you must also be subscribed to the private PPA. |
526 | 53 | sources. | 63 | .TP |
527 | 64 | .B -c, --cloud | ||
528 | 65 | Add an Ubuntu Cloud Archive. | ||
529 | 66 | Must be in the format \fBcloud-archive:CANAME\fR, \fBuca:CANAME\fR, or \fBCANAME\fR. | ||
530 | 67 | The \fBCANAME\fR parameter should be the name of the Cloud Archive. | ||
531 | 68 | The \fBCANAME\fR parameter may optionally be suffixed with the pocket, as either \fB-updates\fR or \fB-proposed\fR. | ||
532 | 69 | If not specified, the pocket defaults to \fB-updates\fR. | ||
533 | 70 | .TP | ||
534 | 71 | .B -U, --uri | ||
535 | 72 | Add an archive, specified as a single URI. | ||
536 | 73 | If the URI provided is detected to be a PPA, this will operate as if the \fI--ppa\fR parameter was used. | ||
537 | 74 | .TP | ||
538 | 75 | .B -S, --sourceslist | ||
539 | 76 | Add an archive, specified as a full source entry line in one-line sources.list format. | ||
540 | 77 | It must follow the \fIONE-LINE-STYLE\fR format as described in the \fBsources.list\fR manpage. | ||
541 | 78 | If the URI provided is detected to be a PPA, this will operate as if the \fI--ppa\fR parameter was used. | ||
542 | 79 | |||
543 | 80 | .SH LINE | ||
544 | 81 | \fILINE\fR is a deprecated method to specify the repository to add/remove, provided only for backwards compatibility. | ||
545 | 82 | It can be specified in any of the supported formats: sources.list line, plain uri, ppa shortcut, or cloud-archive shortcut. | ||
546 | 83 | It can also be specified as one or more valid component(s). The script will attempt to detect which format is provided. | ||
547 | 84 | |||
548 | 85 | This is not recommended as the autodetection of which repository format is intended can be ambiguous, but older | ||
549 | 86 | scripts may still use this method of specifying the repository. | ||
550 | 87 | |||
551 | 88 | One special case of \fILINE\fR is providing the value \fB-\fR, which will then read the \fILINE\fR from stdin. | ||
552 | 89 | |||
553 | 90 | .SH DEPRECATED EXAMPLES | ||
554 | 91 | .TP | ||
555 | 92 | add-apt-repository -p ppa:user/repository | ||
556 | 93 | .TP | ||
557 | 94 | add-apt-repository -p user/repository | ||
558 | 95 | .TP | ||
559 | 96 | add-apt-repository -c cloud-archive:queens | ||
560 | 97 | .TP | ||
561 | 98 | add-apt-repository -c queens | ||
562 | 99 | .TP | ||
563 | 100 | add-apt-repository -S 'deb http://myserver/path/to/repo stable main' | ||
564 | 101 | .TP | ||
565 | 102 | add-apt-repository -U http://myserver/path/to/repo -C main | ||
566 | 103 | .TP | ||
567 | 104 | add-apt-repository -U https://packages.medibuntu.org -C free -C non-free | ||
568 | 105 | .TP | ||
569 | 106 | add-apt-repository -U http://extras.ubuntu.com/ubuntu | ||
570 | 107 | .TP | ||
571 | 108 | add-apt-repository -s | ||
572 | 109 | .TP | ||
573 | 110 | add-apt-repository -s -r | ||
574 | 111 | .TP | ||
575 | 112 | add-apt-repository -C universe | ||
576 | 113 | .TP | ||
577 | 114 | add-apt-repository -r -C multiverse | ||
578 | 115 | |||
579 | 116 | .SH DEPRECATED EXAMPLES | ||
580 | 117 | .TP | ||
581 | 118 | add-apt-repository deb http://myserver/path/to/repo stable main | ||
582 | 119 | .TP | ||
583 | 120 | add-apt-repository http://myserver/path/to/repo main | ||
584 | 121 | .TP | ||
585 | 122 | add-apt-repository https://packages.medibuntu.org free non-free | ||
586 | 123 | .TP | ||
587 | 124 | add-apt-repository http://extras.ubuntu.com/ubuntu | ||
588 | 125 | .TP | ||
589 | 126 | add-apt-repository multiverse | ||
590 | 54 | 127 | ||
591 | 55 | .SH SEE ALSO | 128 | .SH SEE ALSO |
592 | 56 | \fBsources.list\fR(5) | 129 | \fBsources.list\fR(5) |
593 | diff --git a/debian/tests/add-apt-repository b/debian/tests/add-apt-repository | |||
594 | 57 | deleted file mode 100755 | 130 | deleted file mode 100755 |
595 | index 7dab076..0000000 | |||
596 | --- a/debian/tests/add-apt-repository | |||
597 | +++ /dev/null | |||
598 | @@ -1,14 +0,0 @@ | |||
599 | 1 | #!/bin/sh | ||
600 | 2 | set -e | ||
601 | 3 | |||
602 | 4 | for locale in C.UTF-8 C | ||
603 | 5 | do | ||
604 | 6 | export LC_ALL=$locale | ||
605 | 7 | echo LC_ALL=$locale test... | ||
606 | 8 | rm -f /etc/apt/sources.list.d/xnox-ubuntu-nonvirt-*.list | ||
607 | 9 | rm -f /etc/apt/trusted.gpg.d/xnox_ubuntu_nonvirt.gpg | ||
608 | 10 | add-apt-repository ppa:xnox/nonvirt --yes --no-update | ||
609 | 11 | [ -s /etc/apt/sources.list.d/xnox-ubuntu-nonvirt-*.list ] | ||
610 | 12 | [ -s /etc/apt/trusted.gpg.d/xnox_ubuntu_nonvirt.gpg ] | ||
611 | 13 | gpg -q --homedir $(mktemp -d) --no-default-keyring --keyring /etc/apt/trusted.gpg.d/xnox_ubuntu_nonvirt.gpg --fingerprint | ||
612 | 14 | done | ||
613 | diff --git a/debian/tests/add-apt-repository-archive b/debian/tests/add-apt-repository-archive | |||
614 | 15 | new file mode 100755 | 0 | new file mode 100755 |
615 | index 0000000..2907391 | |||
616 | --- /dev/null | |||
617 | +++ b/debian/tests/add-apt-repository-archive | |||
618 | @@ -0,0 +1,56 @@ | |||
619 | 1 | #!/usr/bin/python3 | ||
620 | 2 | |||
621 | 3 | import contextlib | ||
622 | 4 | import os | ||
623 | 5 | import sys | ||
624 | 6 | import subprocess | ||
625 | 7 | import tempfile | ||
626 | 8 | |||
627 | 9 | from aptsources.distro import get_distro | ||
628 | 10 | |||
629 | 11 | |||
630 | 12 | codename=get_distro().codename | ||
631 | 13 | URI='http://fake.mirror.private.com/ubuntu' | ||
632 | 14 | SOURCESLIST=f'deb {URI} {codename} main' | ||
633 | 15 | SOURCESLISTFILE=f'/etc/apt/sources.list.d/archive_uri-http_fake_mirror_private_com_ubuntu-{codename}.list' | ||
634 | 16 | |||
635 | 17 | def run_test(archive, param, yes, noupdate, remove, locale): | ||
636 | 18 | env = os.environ.copy() | ||
637 | 19 | if locale: | ||
638 | 20 | env['LC_ALL'] = locale | ||
639 | 21 | |||
640 | 22 | with contextlib.suppress(FileNotFoundError): | ||
641 | 23 | os.remove(SOURCESLISTFILE) | ||
642 | 24 | |||
643 | 25 | cmd = f'add-apt-repository {yes} {noupdate} {param} {archive}' | ||
644 | 26 | subprocess.check_call(cmd.split(), env=env) | ||
645 | 27 | |||
646 | 28 | if not os.path.exists(SOURCESLISTFILE) or os.path.getsize(SOURCESLISTFILE) == 0: | ||
647 | 29 | print("Missing/empty sources.list file: %s" % SOURCESLISTFILE) | ||
648 | 30 | sys.exit(1) | ||
649 | 31 | |||
650 | 32 | cmd = f'add-apt-repository {remove} {yes} {noupdate} {param} {archive}' | ||
651 | 33 | subprocess.check_call(cmd.split(), env=env) | ||
652 | 34 | |||
653 | 35 | if os.path.exists(SOURCESLISTFILE): | ||
654 | 36 | print("sources.list file not removed: %s" % SOURCESLISTFILE) | ||
655 | 37 | with open(SOURCESLISTFILE) as f: | ||
656 | 38 | print(f.read()) | ||
657 | 39 | sys.exit(1) | ||
658 | 40 | |||
659 | 41 | |||
660 | 42 | for PARAM in ['-U', '--uri', '']: | ||
661 | 43 | for YES in ['-y', '--yes']: | ||
662 | 44 | for NOUPDATE in ['-n', '--no-update', '']: | ||
663 | 45 | for REMOVE in ['-r', '--remove']: | ||
664 | 46 | for LOCALE in ['', 'C', 'C.UTF-8']: | ||
665 | 47 | run_test(URI, PARAM, YES, NOUPDATE, REMOVE, LOCALE) | ||
666 | 48 | |||
667 | 49 | for PARAM in ['-S', '--sourceslist', '']: | ||
668 | 50 | for YES in ['-y', '--yes']: | ||
669 | 51 | for NOUPDATE in ['-n', '--no-update', '']: | ||
670 | 52 | for REMOVE in ['-r', '--remove']: | ||
671 | 53 | for LOCALE in ['', 'C', 'C.UTF-8']: | ||
672 | 54 | run_test(SOURCESLIST, PARAM, YES, NOUPDATE, REMOVE, LOCALE) | ||
673 | 55 | |||
674 | 56 | sys.exit(0) | ||
675 | diff --git a/debian/tests/add-apt-repository-cloud b/debian/tests/add-apt-repository-cloud | |||
676 | 0 | new file mode 100755 | 57 | new file mode 100755 |
677 | index 0000000..93619e0 | |||
678 | --- /dev/null | |||
679 | +++ b/debian/tests/add-apt-repository-cloud | |||
680 | @@ -0,0 +1,59 @@ | |||
681 | 1 | #!/usr/bin/python3 | ||
682 | 2 | |||
683 | 3 | import contextlib | ||
684 | 4 | import os | ||
685 | 5 | import sys | ||
686 | 6 | import subprocess | ||
687 | 7 | |||
688 | 8 | from softwareproperties.cloudarchive import RELEASE_MAP | ||
689 | 9 | from aptsources.distro import get_distro | ||
690 | 10 | |||
691 | 11 | codename = get_distro().codename | ||
692 | 12 | uca_releases = list(filter(lambda r: RELEASE_MAP[r] == codename, RELEASE_MAP.keys())) | ||
693 | 13 | |||
694 | 14 | if not uca_releases: | ||
695 | 15 | print("No UCA releases available for this Ubuntu release") | ||
696 | 16 | sys.exit(77) | ||
697 | 17 | |||
698 | 18 | def run_test(caname, uca, param, yes, noupdate, remove, locale): | ||
699 | 19 | env = os.environ.copy() | ||
700 | 20 | if locale: | ||
701 | 21 | env['LC_ALL'] = locale | ||
702 | 22 | |||
703 | 23 | SOURCESLISTFILE=f'/etc/apt/sources.list.d/cloudarchive-{caname}.list' | ||
704 | 24 | |||
705 | 25 | with contextlib.suppress(FileNotFoundError): | ||
706 | 26 | os.remove(SOURCESLISTFILE) | ||
707 | 27 | |||
708 | 28 | cmd = 'apt-get -q -y remove ubuntu-cloud-keyring' | ||
709 | 29 | subprocess.run(cmd.split(), stderr=subprocess.DEVNULL, env=env) | ||
710 | 30 | |||
711 | 31 | cmd = f'add-apt-repository {yes} {noupdate} {param} {uca}' | ||
712 | 32 | subprocess.check_call(cmd.split(), env=env) | ||
713 | 33 | |||
714 | 34 | if not os.path.exists(SOURCESLISTFILE) or os.path.getsize(SOURCESLISTFILE) == 0: | ||
715 | 35 | print("Missing/empty sources.list file: %s" % SOURCESLISTFILE) | ||
716 | 36 | sys.exit(1) | ||
717 | 37 | |||
718 | 38 | cmd = 'dpkg-query -l ubuntu-cloud-keyring' | ||
719 | 39 | subprocess.check_call(cmd.split(), env=env) | ||
720 | 40 | |||
721 | 41 | cmd = f'add-apt-repository {remove} {yes} {noupdate} {param} {uca}' | ||
722 | 42 | subprocess.check_call(cmd.split(), env=env) | ||
723 | 43 | |||
724 | 44 | if os.path.exists(SOURCESLISTFILE): | ||
725 | 45 | print("sources.list file not removed: %s" % SOURCESLISTFILE) | ||
726 | 46 | with open(SOURCESLISTFILE) as f: | ||
727 | 47 | print(f.read()) | ||
728 | 48 | sys.exit(1) | ||
729 | 49 | |||
730 | 50 | for CANAME in uca_releases: | ||
731 | 51 | for UCA in [f'{CANAME}', f'cloud-archive:{CANAME}', f'uca:{CANAME}']: | ||
732 | 52 | for PARAM in ['-c', '--cloud', '']: | ||
733 | 53 | for YES in ['-y', '--yes']: | ||
734 | 54 | for NOUPDATE in ['-n', '--no-update', '']: | ||
735 | 55 | for REMOVE in ['-r', '--remove']: | ||
736 | 56 | for LOCALE in ['', 'C', 'C.UTF-8']: | ||
737 | 57 | run_test(CANAME, UCA, PARAM, YES, NOUPDATE, REMOVE, LOCALE) | ||
738 | 58 | |||
739 | 59 | sys.exit(0) | ||
740 | diff --git a/debian/tests/add-apt-repository-ppa b/debian/tests/add-apt-repository-ppa | |||
741 | 0 | new file mode 100755 | 60 | new file mode 100755 |
742 | index 0000000..fb78985 | |||
743 | --- /dev/null | |||
744 | +++ b/debian/tests/add-apt-repository-ppa | |||
745 | @@ -0,0 +1,71 @@ | |||
746 | 1 | #!/usr/bin/python3 | ||
747 | 2 | |||
748 | 3 | import contextlib | ||
749 | 4 | import os | ||
750 | 5 | import sys | ||
751 | 6 | import subprocess | ||
752 | 7 | import tempfile | ||
753 | 8 | |||
754 | 9 | from aptsources.distro import get_distro | ||
755 | 10 | |||
756 | 11 | codename=get_distro().codename | ||
757 | 12 | SOURCESLISTFILE=f'/etc/apt/sources.list.d/ubuntu-support-team-ubuntu-software-properties-autopkgtest-{codename}.list' | ||
758 | 13 | TRUSTEDFILE='/etc/apt/trusted.gpg.d/ubuntu-support-team-ubuntu-software-properties-autopkgtest.gpg' | ||
759 | 14 | PPANAME='ubuntu-support-team/software-properties-autopkgtest' | ||
760 | 15 | |||
761 | 16 | def run_test(ppa, param, yes, noupdate, remove, locale): | ||
762 | 17 | env = os.environ.copy() | ||
763 | 18 | if locale: | ||
764 | 19 | env['LC_ALL'] = locale | ||
765 | 20 | |||
766 | 21 | with contextlib.suppress(FileNotFoundError): | ||
767 | 22 | os.remove(SOURCESLISTFILE) | ||
768 | 23 | with contextlib.suppress(FileNotFoundError): | ||
769 | 24 | os.remove(TRUSTEDFILE) | ||
770 | 25 | |||
771 | 26 | cmd = f'add-apt-repository {yes} {noupdate} {param} {ppa}' | ||
772 | 27 | try: | ||
773 | 28 | subprocess.check_call(cmd.split(), env=env) | ||
774 | 29 | except subprocess.CalledProcessError: | ||
775 | 30 | if not param and not ppa.startswith('ppa:'): | ||
776 | 31 | # When using 'line' instead of --ppa, the 'ppa:' prefix is required | ||
777 | 32 | return | ||
778 | 33 | raise | ||
779 | 34 | |||
780 | 35 | if not os.path.exists(SOURCESLISTFILE) or os.path.getsize(SOURCESLISTFILE) == 0: | ||
781 | 36 | print("Missing/empty sources.list file: %s" % SOURCESLISTFILE) | ||
782 | 37 | sys.exit(1) | ||
783 | 38 | |||
784 | 39 | if not os.path.exists(TRUSTEDFILE) or os.path.getsize(TRUSTEDFILE) == 0: | ||
785 | 40 | print("Missing/empty trusted.gpg file: %s" % TRUSTEDFILE) | ||
786 | 41 | sys.exit(1) | ||
787 | 42 | |||
788 | 43 | with tempfile.TemporaryDirectory() as homedir: | ||
789 | 44 | cmd = f'gpg -q --homedir {homedir} --no-default-keyring --keyring {TRUSTEDFILE} --fingerprint' | ||
790 | 45 | subprocess.check_call(cmd.split(), env=env) | ||
791 | 46 | |||
792 | 47 | cmd = f'add-apt-repository {remove} {yes} {noupdate} {param} {ppa}' | ||
793 | 48 | subprocess.check_call(cmd.split(), env=env) | ||
794 | 49 | |||
795 | 50 | if os.path.exists(SOURCESLISTFILE): | ||
796 | 51 | print("sources.list file not removed: %s" % SOURCESLISTFILE) | ||
797 | 52 | with open(SOURCESLISTFILE) as f: | ||
798 | 53 | print(f.read()) | ||
799 | 54 | sys.exit(1) | ||
800 | 55 | |||
801 | 56 | if not os.path.exists(TRUSTEDFILE): | ||
802 | 57 | print("trusted.gpg should not have been removed, but it was: %s" % TRUSTEDFILE) | ||
803 | 58 | sys.exit(1) | ||
804 | 59 | |||
805 | 60 | |||
806 | 61 | for PPAFORMAT in [f'{PPANAME}', f'{PPANAME}'.replace('/', '/ubuntu/')]: | ||
807 | 62 | for PPA in [f'{PPAFORMAT}', f'ppa:{PPAFORMAT}']: | ||
808 | 63 | for PARAM in ['-p', '--ppa', '']: | ||
809 | 64 | for YES in ['-y', '--yes']: | ||
810 | 65 | for NOUPDATE in ['-n', '--no-update', '']: | ||
811 | 66 | for REMOVE in ['-r', '--remove']: | ||
812 | 67 | for LOCALE in ['', 'C', 'C.UTF-8']: | ||
813 | 68 | run_test(PPA, PARAM, YES, NOUPDATE, REMOVE, LOCALE) | ||
814 | 69 | |||
815 | 70 | sys.exit(0) | ||
816 | 71 | |||
817 | diff --git a/debian/tests/control b/debian/tests/control | |||
818 | index c90e3ef..ad74b04 100644 | |||
819 | --- a/debian/tests/control | |||
820 | +++ b/debian/tests/control | |||
821 | @@ -11,6 +11,14 @@ Depends: dbus-x11, | |||
822 | 11 | xvfb, | 11 | xvfb, |
823 | 12 | @ | 12 | @ |
824 | 13 | 13 | ||
826 | 14 | Tests: add-apt-repository | 14 | Tests: add-apt-repository-ppa |
827 | 15 | Depends: gpg, software-properties-common | 15 | Depends: gpg, software-properties-common |
829 | 16 | Restrictions: needs-root, breaks-testbed | 16 | Restrictions: needs-root, breaks-testbed, allow-stderr |
830 | 17 | |||
831 | 18 | Tests: add-apt-repository-cloud | ||
832 | 19 | Depends: software-properties-common | ||
833 | 20 | Restrictions: needs-root, breaks-testbed, allow-stderr, skippable | ||
834 | 21 | |||
835 | 22 | Tests: add-apt-repository-archive | ||
836 | 23 | Depends: software-properties-common | ||
837 | 24 | Restrictions: needs-root, breaks-testbed, allow-stderr | ||
838 | diff --git a/softwareproperties/SoftwareProperties.py b/softwareproperties/SoftwareProperties.py | |||
839 | index fae6d18..30c0289 100644 | |||
840 | --- a/softwareproperties/SoftwareProperties.py | |||
841 | +++ b/softwareproperties/SoftwareProperties.py | |||
842 | @@ -60,19 +60,12 @@ import aptsources.distro | |||
843 | 60 | import softwareproperties | 60 | import softwareproperties |
844 | 61 | 61 | ||
845 | 62 | from .AptAuth import AptAuth | 62 | from .AptAuth import AptAuth |
850 | 63 | from aptsources.sourceslist import SourcesList, SourceEntry | 63 | from aptsources.sourceslist import (SourcesList, SourceEntry) |
851 | 64 | from . import shortcuts | 64 | from softwareproperties.shortcuthandler import (ShortcutException, InvalidShortcutException) |
852 | 65 | from . import ppa | 65 | from softwareproperties.shortcuts import shortcut_handler |
849 | 66 | from . import cloudarchive | ||
853 | 67 | 66 | ||
854 | 68 | from gi.repository import Gio | 67 | from gi.repository import Gio |
855 | 69 | 68 | ||
856 | 70 | _SHORTCUT_FACTORIES = [ | ||
857 | 71 | ppa.shortcut_handler, | ||
858 | 72 | cloudarchive.shortcut_handler, | ||
859 | 73 | shortcuts.shortcut_handler, | ||
860 | 74 | ] | ||
861 | 75 | |||
862 | 76 | 69 | ||
863 | 77 | class SoftwareProperties(object): | 70 | class SoftwareProperties(object): |
864 | 78 | 71 | ||
865 | @@ -678,31 +671,25 @@ class SoftwareProperties(object): | |||
866 | 678 | return os.path.splitext(os.path.basename(f))[0] | 671 | return os.path.splitext(os.path.basename(f))[0] |
867 | 679 | return None | 672 | return None |
868 | 680 | 673 | ||
869 | 681 | def check_and_add_key_for_whitelisted_channels(self, srcline): | ||
870 | 682 | # This is maintained for any legacy callers | ||
871 | 683 | return self.check_and_add_key_for_whitelisted_shortcut(shortcut_handler(srcline)) | ||
872 | 684 | |||
873 | 685 | def check_and_add_key_for_whitelisted_shortcut(self, shortcut): | 674 | def check_and_add_key_for_whitelisted_shortcut(self, shortcut): |
874 | 686 | """ | 675 | """ |
875 | 687 | helper that adds the gpg key of the channel to the apt | 676 | helper that adds the gpg key of the channel to the apt |
876 | 688 | keyring *if* the channel is in the whitelist | 677 | keyring *if* the channel is in the whitelist |
877 | 689 | /usr/share/app-install/channels or it is a public Launchpad PPA. | 678 | /usr/share/app-install/channels or it is a public Launchpad PPA. |
878 | 690 | """ | 679 | """ |
881 | 691 | (srcline, _fname) = shortcut.expand( | 680 | srcline = shortcut.SourceEntry().line |
880 | 692 | codename=self.distro.codename, distro=self.distro.id.lower()) | ||
882 | 693 | channel = self._is_line_in_whitelisted_channel(srcline) | 681 | channel = self._is_line_in_whitelisted_channel(srcline) |
883 | 694 | if channel: | 682 | if channel: |
884 | 695 | keyp = "%s/%s.key" % (self.CHANNEL_PATH, channel) | 683 | keyp = "%s/%s.key" % (self.CHANNEL_PATH, channel) |
885 | 696 | self.add_key(keyp) | 684 | self.add_key(keyp) |
886 | 697 | 685 | ||
888 | 698 | cdata = (shortcut.add_key, {'keyserver': (self.options)}) | 686 | cdata = (shortcut.add_key, {}) |
889 | 699 | def addkey_func(): | 687 | def addkey_func(): |
890 | 700 | func, kwargs = cdata | 688 | func, kwargs = cdata |
891 | 701 | msg = "Added key." | 689 | msg = "Added key." |
892 | 702 | try: | 690 | try: |
896 | 703 | ret = func(**kwargs) | 691 | func(**kwargs) |
897 | 704 | if not ret: | 692 | ret = True |
895 | 705 | msg = "Failed to add key." | ||
898 | 706 | except Exception as e: | 693 | except Exception as e: |
899 | 707 | ret = False | 694 | ret = False |
900 | 708 | msg = str(e) | 695 | msg = str(e) |
901 | @@ -736,9 +723,12 @@ class SoftwareProperties(object): | |||
902 | 736 | """ | 723 | """ |
903 | 737 | Add a source for the given line. | 724 | Add a source for the given line. |
904 | 738 | """ | 725 | """ |
908 | 739 | return self.add_source_from_shortcut( | 726 | try: |
909 | 740 | shortcut=shortcut_handler(line.strip()), | 727 | shortcut = shortcut_handler(line.strip()) |
910 | 741 | enable_source_code=enable_source_code) | 728 | except InvalidShortcutException: |
911 | 729 | return False | ||
912 | 730 | |||
913 | 731 | return self.add_source_from_shortcut(shortcut, enable_source_code) | ||
914 | 742 | 732 | ||
915 | 743 | def add_source_from_shortcut(self, shortcut, enable_source_code=False): | 733 | def add_source_from_shortcut(self, shortcut, enable_source_code=False): |
916 | 744 | """ | 734 | """ |
917 | @@ -746,8 +736,8 @@ class SoftwareProperties(object): | |||
918 | 746 | site is in whitelist or the shortcut implementer adds it. | 736 | site is in whitelist or the shortcut implementer adds it. |
919 | 747 | """ | 737 | """ |
920 | 748 | 738 | ||
923 | 749 | (deb_line, file) = shortcut.expand( | 739 | deb_line = shortcut.SourceEntry().line |
924 | 750 | codename=self.distro.codename, distro=self.distro.id.lower()) | 740 | file = shortcut.sourceparts_file |
925 | 751 | deb_line = self.expand_http_line(deb_line) | 741 | deb_line = self.expand_http_line(deb_line) |
926 | 752 | debsrc_entry_type = 'deb-src' if enable_source_code else '# deb-src' | 742 | debsrc_entry_type = 'deb-src' if enable_source_code else '# deb-src' |
927 | 753 | debsrc_line = debsrc_entry_type + deb_line[3:] | 743 | debsrc_line = debsrc_entry_type + deb_line[3:] |
928 | @@ -776,10 +766,10 @@ class SoftwareProperties(object): | |||
929 | 776 | worker.join(30) | 766 | worker.join(30) |
930 | 777 | if worker.isAlive(): | 767 | if worker.isAlive(): |
931 | 778 | # thread timed out. | 768 | # thread timed out. |
933 | 779 | raise shortcuts.ShortcutException("Error: retrieving gpg key timed out.") | 769 | raise ShortcutException("Error: retrieving gpg key timed out.") |
934 | 780 | result, msg = self.myqueue.get() | 770 | result, msg = self.myqueue.get() |
935 | 781 | if not result: | 771 | if not result: |
937 | 782 | raise shortcuts.ShortcutException(msg) | 772 | raise ShortcutException(msg) |
938 | 783 | 773 | ||
939 | 784 | if self.options and self.options.update: | 774 | if self.options and self.options.update: |
940 | 785 | import apt | 775 | import apt |
941 | @@ -866,14 +856,6 @@ class SoftwareProperties(object): | |||
942 | 866 | return "%s;%s;%s;" % (ver.package.shortname, ver.version, | 856 | return "%s;%s;%s;" % (ver.package.shortname, ver.version, |
943 | 867 | ver.package.architecture()) | 857 | ver.package.architecture()) |
944 | 868 | 858 | ||
945 | 869 | def shortcut_handler(shortcut): | ||
946 | 870 | for factory in _SHORTCUT_FACTORIES: | ||
947 | 871 | ret = factory(shortcut) | ||
948 | 872 | if ret is not None: | ||
949 | 873 | return ret | ||
950 | 874 | |||
951 | 875 | raise shortcuts.ShortcutException("Unable to handle input '%s'" % shortcut) | ||
952 | 876 | |||
953 | 877 | 859 | ||
954 | 878 | if __name__ == "__main__": | 860 | if __name__ == "__main__": |
955 | 879 | sp = SoftwareProperties() | 861 | sp = SoftwareProperties() |
956 | diff --git a/softwareproperties/cloudarchive.py b/softwareproperties/cloudarchive.py | |||
957 | index 476452d..b59ace0 100644 | |||
958 | --- a/softwareproperties/cloudarchive.py | |||
959 | +++ b/softwareproperties/cloudarchive.py | |||
960 | @@ -21,12 +21,17 @@ | |||
961 | 21 | 21 | ||
962 | 22 | from __future__ import print_function | 22 | from __future__ import print_function |
963 | 23 | 23 | ||
964 | 24 | import apt_pkg | ||
965 | 25 | import os | 24 | import os |
967 | 26 | import subprocess | 25 | |
968 | 27 | from gettext import gettext as _ | 26 | from gettext import gettext as _ |
969 | 28 | 27 | ||
971 | 29 | from softwareproperties.shortcuts import ShortcutException | 28 | from softwareproperties.shortcuthandler import (ShortcutHandler, ShortcutException, |
972 | 29 | InvalidShortcutException) | ||
973 | 30 | from softwareproperties.sourceslist import SourcesListShortcutHandler | ||
974 | 31 | from softwareproperties.uri import URIShortcutHandler | ||
975 | 32 | |||
976 | 33 | from urllib.parse import urlparse | ||
977 | 34 | |||
978 | 30 | 35 | ||
979 | 31 | RELEASE_MAP = { | 36 | RELEASE_MAP = { |
980 | 32 | 'folsom': 'precise', | 37 | 'folsom': 'precise', |
981 | @@ -46,98 +51,109 @@ RELEASE_MAP = { | |||
982 | 46 | 'train': 'bionic', | 51 | 'train': 'bionic', |
983 | 47 | 'ussuri': 'bionic', | 52 | 'ussuri': 'bionic', |
984 | 48 | } | 53 | } |
985 | 49 | MIRROR = "http://ubuntu-cloud.archive.canonical.com/ubuntu" | ||
986 | 50 | UCA = "Ubuntu Cloud Archive" | 54 | UCA = "Ubuntu Cloud Archive" |
987 | 51 | WEB_LINK = 'https://wiki.ubuntu.com/OpenStack/CloudArchive' | 55 | WEB_LINK = 'https://wiki.ubuntu.com/OpenStack/CloudArchive' |
988 | 52 | APT_INSTALL_KEY = ['apt-get', '--quiet', '--assume-yes', 'install', | ||
989 | 53 | 'ubuntu-cloud-keyring'] | ||
990 | 54 | |||
991 | 55 | ALIASES = {'tools-updates': 'tools'} | ||
992 | 56 | for _r in RELEASE_MAP: | ||
993 | 57 | ALIASES["%s-updates" % _r] = _r | ||
994 | 58 | |||
995 | 59 | MAP = { | ||
996 | 60 | 'tools': { | ||
997 | 61 | 'sldfmt': '%(codename)s-updates/cloud-tools', | ||
998 | 62 | 'description': UCA + " for cloud-tools (JuJu and MAAS)"}, | ||
999 | 63 | 'tools-proposed': { | ||
1000 | 64 | 'sldfmt': '%(codename)s-proposed/cloud-tools', | ||
1001 | 65 | 'description': UCA + " for cloud-tools (JuJu and MAAS) [proposed]"} | ||
1002 | 66 | } | ||
1003 | 67 | |||
1004 | 68 | for _r in RELEASE_MAP: | ||
1005 | 69 | MAP[_r] = { | ||
1006 | 70 | 'sldfmt': '%(codename)s-updates/' + _r, | ||
1007 | 71 | 'description': UCA + ' for ' + 'OpenStack ' + _r.capitalize(), | ||
1008 | 72 | 'release': RELEASE_MAP[_r]} | ||
1009 | 73 | MAP[_r + "-proposed"] = { | ||
1010 | 74 | 'sldfmt': '%(codename)s-proposed/' + _r, | ||
1011 | 75 | 'description': UCA + ' for ' + 'OpenStack %s [proposed]' % _r.capitalize(), | ||
1012 | 76 | 'release': RELEASE_MAP[_r]} | ||
1013 | 77 | |||
1014 | 78 | 56 | ||
1030 | 79 | class CloudArchiveShortcutHandler(object): | 57 | UCA_ARCHIVE = "http://ubuntu-cloud.archive.canonical.com/ubuntu" |
1031 | 80 | def __init__(self, shortcut): | 58 | UCA_PREFIXES = ['cloud-archive', 'uca'] |
1032 | 81 | self.shortcut = shortcut | 59 | UCA_VALID_POCKETS = ['updates', 'proposed'] |
1033 | 82 | 60 | UCA_DEFAULT_POCKET = UCA_VALID_POCKETS[0] | |
1034 | 83 | prefix = "cloud-archive:" | 61 | |
1035 | 84 | 62 | ||
1036 | 85 | subs = {'shortcut': shortcut, 'prefix': prefix, | 63 | class CloudArchiveShortcutHandler(ShortcutHandler): |
1037 | 86 | 'ca_names': sorted(MAP.keys())} | 64 | def __init__(self, shortcut, **kwargs): |
1038 | 87 | if not shortcut.startswith(prefix): | 65 | super(CloudArchiveShortcutHandler, self).__init__(shortcut, **kwargs) |
1039 | 88 | raise ValueError( | 66 | self.caname = None |
1040 | 89 | _("shortcut '%(shortcut)s' did not start with '%(prefix)s'") | 67 | self.pocket = None |
1041 | 90 | % subs) | 68 | |
1042 | 91 | 69 | # one of these will set caname and pocket, and maybe _source_entry | |
1043 | 92 | name_in = shortcut[len(prefix):] | 70 | if not any((self._match_uca(shortcut), |
1044 | 93 | caname = ALIASES.get(name_in, name_in) | 71 | self._match_uri(shortcut), |
1045 | 72 | self._match_sourceslist(shortcut))): | ||
1046 | 73 | msg = (_("not a valid cloud-archive format: '%s'") % shortcut) | ||
1047 | 74 | raise InvalidShortcutException(msg) | ||
1048 | 75 | |||
1049 | 76 | self.caname = self.caname.lower() | ||
1050 | 77 | |||
1051 | 78 | self._filebase = "cloudarchive-%s" % self.caname | ||
1052 | 79 | |||
1053 | 80 | self.pocket = self.pocket.lower() | ||
1054 | 81 | if not self.pocket in UCA_VALID_POCKETS: | ||
1055 | 82 | msg = (_("not a valid cloud-archive pocket: '%s'") % self.pocket) | ||
1056 | 83 | raise ShortcutException(msg) | ||
1057 | 84 | |||
1058 | 85 | if not self.caname in RELEASE_MAP: | ||
1059 | 86 | msg = (_("not a valid cloud-archive: '%s'") % self.caname) | ||
1060 | 87 | raise ShortcutException(msg) | ||
1061 | 88 | |||
1062 | 89 | codename = RELEASE_MAP[self.caname] | ||
1063 | 90 | validnames = (self.codename, os.getenv("CA_ALLOW_CODENAME")) | ||
1064 | 91 | if codename not in validnames: | ||
1065 | 92 | msg = (_("cloud-archive for %s only supported on %s") % | ||
1066 | 93 | (self.caname.capitalize(), codename.capitalize())) | ||
1067 | 94 | raise ShortcutException(msg) | ||
1068 | 95 | |||
1069 | 96 | self._description = f'{UCA} for OpenStack {self.caname.capitalize()}' | ||
1070 | 97 | if self.pocket == 'proposed': | ||
1071 | 98 | self._description += ' [proposed]' | ||
1072 | 99 | |||
1073 | 100 | if not self._source_entry: | ||
1074 | 101 | dist = ('%s-%s/%s' % (codename, self.pocket, self.caname)) | ||
1075 | 102 | comps = ' '.join(self.components) or 'main' | ||
1076 | 103 | line = ' '.join([self.distro.binary_type, UCA_ARCHIVE, dist, comps]) | ||
1077 | 104 | self._set_source_entry(line) | ||
1078 | 105 | |||
1079 | 106 | @property | ||
1080 | 107 | def description(self): | ||
1081 | 108 | return self._description | ||
1082 | 109 | |||
1083 | 110 | @property | ||
1084 | 111 | def web_link(self): | ||
1085 | 112 | return WEB_LINK | ||
1086 | 113 | |||
1087 | 114 | def _encode_filebase(self, suffix=None): | ||
1088 | 115 | # ignore suffix | ||
1089 | 116 | return super(CloudArchiveShortcutHandler, self)._encode_filebase() | ||
1090 | 117 | |||
1091 | 118 | def _match_uca(self, shortcut): | ||
1092 | 119 | (prefix, _, uca) = shortcut.rpartition(':') | ||
1093 | 120 | if not prefix.lower() in UCA_PREFIXES: | ||
1094 | 121 | return False | ||
1095 | 94 | 122 | ||
1101 | 95 | subs.update({'input_name': name_in}) | 123 | (caname, _, pocket) = uca.partition('-') |
1102 | 96 | if caname not in MAP: | 124 | if not caname: |
1103 | 97 | raise ShortcutException( | 125 | return False |
1099 | 98 | _("'%(input_name)s': not a valid cloud-archive name.\n" | ||
1100 | 99 | "Must be one of %(ca_names)s") % subs) | ||
1104 | 100 | 126 | ||
1105 | 101 | self.caname = caname | 127 | self.caname = caname |
1124 | 102 | self._info = MAP[caname].copy() | 128 | self.pocket = pocket or UCA_DEFAULT_POCKET |
1107 | 103 | self._info['web_link' ] = WEB_LINK | ||
1108 | 104 | |||
1109 | 105 | def info(self): | ||
1110 | 106 | return self._info | ||
1111 | 107 | |||
1112 | 108 | def expand(self, codename, distro=None): | ||
1113 | 109 | if codename not in (MAP[self.caname]['release'], | ||
1114 | 110 | os.environ.get("CA_ALLOW_CODENAME")): | ||
1115 | 111 | raise ShortcutException( | ||
1116 | 112 | _("cloud-archive for %(os_release)s only supported on %(codename)s") | ||
1117 | 113 | % {'codename': MAP[self.caname]['release'], | ||
1118 | 114 | 'os_release': self.caname.capitalize()}) | ||
1119 | 115 | dist = MAP[self.caname]['sldfmt'] % {'codename': codename} | ||
1120 | 116 | line = ' '.join(('deb', MIRROR, dist, 'main',)) | ||
1121 | 117 | return (line, _fname_for_caname(self.caname)) | ||
1122 | 118 | |||
1123 | 119 | def should_confirm(self): | ||
1125 | 120 | return True | 129 | return True |
1126 | 121 | 130 | ||
1130 | 122 | def add_key(self, keyserver=None): | 131 | def _match_uri(self, shortcut): |
1128 | 123 | env = os.environ.copy() | ||
1129 | 124 | env['DEBIAN_FRONTEND'] = 'noninteractive' | ||
1131 | 125 | try: | 132 | try: |
1134 | 126 | subprocess.check_call(args=APT_INSTALL_KEY, env=env) | 133 | return self._match_handler(URIShortcutHandler(shortcut)) |
1135 | 127 | except subprocess.CalledProcessError: | 134 | except InvalidShortcutException: |
1136 | 135 | return False | ||
1137 | 136 | |||
1138 | 137 | def _match_sourceslist(self, shortcut): | ||
1139 | 138 | try: | ||
1140 | 139 | return self._match_handler(SourcesListShortcutHandler(shortcut)) | ||
1141 | 140 | except InvalidShortcutException: | ||
1142 | 141 | return False | ||
1143 | 142 | |||
1144 | 143 | def _match_handler(self, handler): | ||
1145 | 144 | parsed = urlparse(handler.SourceEntry().uri) | ||
1146 | 145 | if parsed.hostname != urlparse(UCA_ARCHIVE).hostname: | ||
1147 | 128 | return False | 146 | return False |
1148 | 129 | return True | ||
1149 | 130 | 147 | ||
1150 | 148 | (codename, _, caname) = handler.SourceEntry().dist.partition('/') | ||
1151 | 149 | (codename, _, pocket) = codename.partition('-') | ||
1152 | 131 | 150 | ||
1158 | 132 | def _fname_for_caname(caname): | 151 | if not all((codename, caname)): |
1159 | 133 | # caname is an entry in MAP ('tools' or 'tools-proposed') | 152 | return False |
1155 | 134 | return os.path.join( | ||
1156 | 135 | apt_pkg.config.find_dir("Dir::Etc::sourceparts"), | ||
1157 | 136 | 'cloudarchive-%s.list' % caname) | ||
1160 | 137 | 153 | ||
1161 | 154 | self.caname = caname | ||
1162 | 155 | self.pocket = pocket or UCA_DEFAULT_POCKET | ||
1163 | 156 | |||
1164 | 157 | self._set_source_entry(handler.SourceEntry().line) | ||
1165 | 158 | return True | ||
1166 | 138 | 159 | ||
1167 | 139 | def shortcut_handler(shortcut): | ||
1168 | 140 | try: | ||
1169 | 141 | return CloudArchiveShortcutHandler(shortcut) | ||
1170 | 142 | except ValueError: | ||
1171 | 143 | return None | ||
1172 | diff --git a/softwareproperties/gtk/DialogCacheOutdated.py b/softwareproperties/gtk/DialogCacheOutdated.py | |||
1173 | index 5852897..9ee671e 100644 | |||
1174 | --- a/softwareproperties/gtk/DialogCacheOutdated.py | |||
1175 | +++ b/softwareproperties/gtk/DialogCacheOutdated.py | |||
1176 | @@ -81,9 +81,8 @@ class DialogCacheOutdated: | |||
1177 | 81 | self._pdia.progressbar.set_fraction(perc / 100.0) | 81 | self._pdia.progressbar.set_fraction(perc / 100.0) |
1178 | 82 | 82 | ||
1179 | 83 | def on_pktask_finish(self, source, result, udata=(None,)): | 83 | def on_pktask_finish(self, source, result, udata=(None,)): |
1180 | 84 | results = None | ||
1181 | 85 | try: | 84 | try: |
1183 | 86 | results = self._pktask.generic_finish(result) | 85 | self._pktask.generic_finish(result) |
1184 | 87 | except Exception as e: | 86 | except Exception as e: |
1185 | 88 | dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, | 87 | dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, |
1186 | 89 | Gtk.ButtonsType.CANCEL, _("Error while refreshing cache")) | 88 | Gtk.ButtonsType.CANCEL, _("Error while refreshing cache")) |
1187 | diff --git a/softwareproperties/gtk/SoftwarePropertiesGtk.py b/softwareproperties/gtk/SoftwarePropertiesGtk.py | |||
1188 | index ae35ec0..a3021f6 100644 | |||
1189 | --- a/softwareproperties/gtk/SoftwarePropertiesGtk.py | |||
1190 | +++ b/softwareproperties/gtk/SoftwarePropertiesGtk.py | |||
1191 | @@ -1072,9 +1072,8 @@ class SoftwarePropertiesGtk(SoftwareProperties, SimpleGtkbuilderApp): | |||
1192 | 1072 | self.progress_bar.set_fraction(prog_value / 100.0) | 1072 | self.progress_bar.set_fraction(prog_value / 100.0) |
1193 | 1073 | 1073 | ||
1194 | 1074 | def on_driver_changes_finish(self, source, result, installs_pending): | 1074 | def on_driver_changes_finish(self, source, result, installs_pending): |
1195 | 1075 | results = None | ||
1196 | 1076 | try: | 1075 | try: |
1198 | 1077 | results = self.pk_task.generic_finish(result) | 1076 | self.pk_task.generic_finish(result) |
1199 | 1078 | except Exception as e: | 1077 | except Exception as e: |
1200 | 1079 | self.on_driver_changes_revert() | 1078 | self.on_driver_changes_revert() |
1201 | 1080 | error(self.window_main, _("Error while applying changes"), str(e)) | 1079 | error(self.window_main, _("Error while applying changes"), str(e)) |
1202 | diff --git a/softwareproperties/kde/.keep b/softwareproperties/kde/.keep | |||
1203 | 1081 | deleted file mode 100644 | 1080 | deleted file mode 100644 |
1204 | index e69de29..0000000 | |||
1205 | --- a/softwareproperties/kde/.keep | |||
1206 | +++ /dev/null | |||
1207 | diff --git a/softwareproperties/ppa.py b/softwareproperties/ppa.py | |||
1208 | index 9ee8df8..351a609 100644 | |||
1209 | --- a/softwareproperties/ppa.py | |||
1210 | +++ b/softwareproperties/ppa.py | |||
1211 | @@ -1,8 +1,9 @@ | |||
1213 | 1 | # software-properties PPA support | 1 | # software-properties PPA support, using launchpadlib |
1214 | 2 | # | 2 | # |
1216 | 3 | # Copyright (c) 2004-2009 Canonical Ltd. | 3 | # Copyright (c) 2019 Canonical Ltd. |
1217 | 4 | # | 4 | # |
1219 | 5 | # Author: Michael Vogt <mvo@debian.org> | 5 | # Original Author: Michael Vogt <mvo@debian.org> |
1220 | 6 | # Rewrite: Dan Streetman <ddstreet@canonical.com> | ||
1221 | 6 | # | 7 | # |
1222 | 7 | # This program is free software; you can redistribute it and/or | 8 | # This program is free software; you can redistribute it and/or |
1223 | 8 | # modify it under the terms of the GNU General Public License as | 9 | # modify it under the terms of the GNU General Public License as |
1224 | @@ -19,457 +20,176 @@ | |||
1225 | 19 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 | 20 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 |
1226 | 20 | # USA | 21 | # USA |
1227 | 21 | 22 | ||
1228 | 22 | from __future__ import print_function | ||
1229 | 23 | |||
1230 | 24 | import apt_pkg | ||
1231 | 25 | import json | ||
1232 | 26 | import os | ||
1233 | 27 | import re | ||
1234 | 28 | import shutil | ||
1235 | 29 | import subprocess | ||
1236 | 30 | import tempfile | ||
1237 | 31 | import time | ||
1238 | 32 | |||
1239 | 33 | from gettext import gettext as _ | 23 | from gettext import gettext as _ |
1240 | 34 | from threading import Thread | ||
1241 | 35 | |||
1242 | 36 | from softwareproperties.shortcuts import ShortcutException | ||
1243 | 37 | |||
1244 | 38 | try: | ||
1245 | 39 | import urllib.request | ||
1246 | 40 | from urllib.error import HTTPError, URLError | ||
1247 | 41 | import urllib.parse | ||
1248 | 42 | from http.client import HTTPException | ||
1249 | 43 | NEED_PYCURL = False | ||
1250 | 44 | except ImportError: | ||
1251 | 45 | NEED_PYCURL = True | ||
1252 | 46 | import pycurl | ||
1253 | 47 | HTTPError = pycurl.error | ||
1254 | 48 | |||
1255 | 49 | |||
1256 | 50 | SKS_KEYSERVER = 'https://keyserver.ubuntu.com/pks/lookup?op=get&options=mr&exact=on&search=0x%s' | ||
1257 | 51 | # maintained until 2015 | ||
1258 | 52 | LAUNCHPAD_PPA_API = 'https://launchpad.net/api/devel/%s/+archive/%s' | ||
1259 | 53 | LAUNCHPAD_USER_API = 'https://launchpad.net/api/1.0/%s' | ||
1260 | 54 | LAUNCHPAD_USER_PPAS_API = 'https://launchpad.net/api/1.0/%s/ppas' | ||
1261 | 55 | LAUNCHPAD_DISTRIBUTION_API = 'https://launchpad.net/api/1.0/%s' | ||
1262 | 56 | LAUNCHPAD_DISTRIBUTION_SERIES_API = 'https://launchpad.net/api/1.0/%s/%s' | ||
1263 | 57 | # Specify to use the system default SSL store; change to a different path | ||
1264 | 58 | # to test with custom certificates. | ||
1265 | 59 | LAUNCHPAD_PPA_CERT = "/etc/ssl/certs/ca-certificates.crt" | ||
1266 | 60 | |||
1267 | 61 | |||
1268 | 62 | class CurlCallback: | ||
1269 | 63 | def __init__(self): | ||
1270 | 64 | self.contents = '' | ||
1271 | 65 | |||
1272 | 66 | def body_callback(self, buf): | ||
1273 | 67 | self.contents = self.contents + buf | ||
1274 | 68 | |||
1275 | 69 | |||
1276 | 70 | class PPAException(Exception): | ||
1277 | 71 | |||
1278 | 72 | def __init__(self, value, original_error=None): | ||
1279 | 73 | self.value = value | ||
1280 | 74 | self.original_error = original_error | ||
1281 | 75 | |||
1282 | 76 | def __str__(self): | ||
1283 | 77 | return repr(self.value) | ||
1284 | 78 | |||
1285 | 79 | |||
1286 | 80 | def encode(s): | ||
1287 | 81 | return re.sub("[^a-zA-Z0-9_-]", "_", s) | ||
1288 | 82 | |||
1289 | 83 | |||
1290 | 84 | def get_info_from_https(url, accept_json, retry_delays=None): | ||
1291 | 85 | """Return the content from url. | ||
1292 | 86 | accept_json indicates that: | ||
1293 | 87 | a.) Send header Accept: 'application/json' | ||
1294 | 88 | b.) Instead of raw content, return json.loads(content) | ||
1295 | 89 | retry_delays is None or an iterator (including list or tuple) | ||
1296 | 90 | If it is None, no retries will be done. | ||
1297 | 91 | If it is an iterator, each value is number of seconds to delay before | ||
1298 | 92 | retrying. For example, retry_delays=(3,5) means to try up to 3 | ||
1299 | 93 | times, with a 3s delay after first failure and 5s delay after second. | ||
1300 | 94 | Retries will not be done on 404.""" | ||
1301 | 95 | func = _get_https_content_pycurl if NEED_PYCURL else _get_https_content_py3 | ||
1302 | 96 | data = func(lp_url=url, accept_json=accept_json, retry_delays=retry_delays) | ||
1303 | 97 | if accept_json: | ||
1304 | 98 | return json.loads(data) | ||
1305 | 99 | else: | ||
1306 | 100 | return data | ||
1307 | 101 | |||
1308 | 102 | |||
1309 | 103 | def get_info_from_lp(lp_url): | ||
1310 | 104 | return get_info_from_https(lp_url, True) | ||
1311 | 105 | |||
1312 | 106 | def get_ppa_info_from_lp(owner_name, ppa): | ||
1313 | 107 | if owner_name[0] != '~': | ||
1314 | 108 | owner_name = '~' + owner_name | ||
1315 | 109 | lp_url = LAUNCHPAD_PPA_API % (owner_name, ppa) | ||
1316 | 110 | return get_info_from_lp(lp_url) | ||
1317 | 111 | |||
1318 | 112 | def series_valid_for_distro(distribution, series): | ||
1319 | 113 | lp_url = LAUNCHPAD_DISTRIBUTION_SERIES_API % (distribution, series) | ||
1320 | 114 | try: | ||
1321 | 115 | get_info_from_lp(lp_url) | ||
1322 | 116 | return True | ||
1323 | 117 | except PPAException: | ||
1324 | 118 | return False | ||
1325 | 119 | 24 | ||
1329 | 120 | def get_current_series_from_lp(distribution): | 25 | from launchpadlib.launchpad import Launchpad |
1330 | 121 | lp_url = LAUNCHPAD_DISTRIBUTION_API % distribution | 26 | from lazr.restfulclient.errors import (NotFound, BadRequest, Unauthorized) |
1328 | 122 | return os.path.basename(get_info_from_lp(lp_url)["current_series_link"]) | ||
1331 | 123 | 27 | ||
1332 | 28 | from softwareproperties.shortcuthandler import (ShortcutHandler, ShortcutException, | ||
1333 | 29 | InvalidShortcutException) | ||
1334 | 30 | from softwareproperties.sourceslist import SourcesListShortcutHandler | ||
1335 | 31 | from softwareproperties.uri import URIShortcutHandler | ||
1336 | 124 | 32 | ||
1340 | 125 | def _get_https_content_py3(lp_url, accept_json, retry_delays=None): | 33 | from urllib.parse import urlparse |
1338 | 126 | if retry_delays is None: | ||
1339 | 127 | retry_delays = [] | ||
1341 | 128 | 34 | ||
1342 | 129 | trynum = 0 | ||
1343 | 130 | err = None | ||
1344 | 131 | sleep_waits = iter(retry_delays) | ||
1345 | 132 | headers = {"Accept": "application/json"} if accept_json else {} | ||
1346 | 133 | 35 | ||
1365 | 134 | while True: | 36 | PPA_URI_FORMAT = 'http://ppa.launchpad.net/{team}/{ppa}/ubuntu/' |
1366 | 135 | trynum += 1 | 37 | PRIVATE_PPA_URI_FORMAT = 'https://private-ppa.launchpad.net/{team}/{ppa}/ubuntu/' |
1367 | 136 | try: | 38 | PPA_VALID_HOSTNAMES = [urlparse(PPA_URI_FORMAT).hostname, urlparse(PRIVATE_PPA_URI_FORMAT).hostname] |
1350 | 137 | request = urllib.request.Request(str(lp_url), headers=headers) | ||
1351 | 138 | lp_page = urllib.request.urlopen(request, | ||
1352 | 139 | cafile=LAUNCHPAD_PPA_CERT) | ||
1353 | 140 | return lp_page.read().decode("utf-8", "strict") | ||
1354 | 141 | except (HTTPException, URLError) as e: | ||
1355 | 142 | err = PPAException( | ||
1356 | 143 | "Error reading %s (%d tries): %s" % (lp_url, trynum, e.reason), | ||
1357 | 144 | e) | ||
1358 | 145 | # do not retry on 404. HTTPError is a subclass of URLError. | ||
1359 | 146 | if isinstance(e, HTTPError) and e.code == 404: | ||
1360 | 147 | break | ||
1361 | 148 | try: | ||
1362 | 149 | time.sleep(next(sleep_waits)) | ||
1363 | 150 | except StopIteration: | ||
1364 | 151 | break | ||
1368 | 152 | 39 | ||
1369 | 153 | raise err | ||
1370 | 154 | 40 | ||
1371 | 41 | class PPAShortcutHandler(ShortcutHandler): | ||
1372 | 42 | def __init__(self, shortcut, login=False, **kwargs): | ||
1373 | 43 | super(PPAShortcutHandler, self).__init__(shortcut, **kwargs) | ||
1374 | 44 | self._lp_anon = not login | ||
1375 | 45 | self._signing_key_data = None | ||
1376 | 155 | 46 | ||
1381 | 156 | def _get_https_content_pycurl(lp_url, accept_json, retry_delays=None): | 47 | self._lp = None # LP object |
1382 | 157 | # this is the fallback code for python2 | 48 | self._lpteam = None # Person/Team LP object |
1383 | 158 | if retry_delays is None: | 49 | self._lpppa = None # PPA Archive LP object |
1380 | 159 | retry_delays = [] | ||
1384 | 160 | 50 | ||
1387 | 161 | trynum = 0 | 51 | # one of these will set teamname and ppaname, and maybe _source_entry |
1388 | 162 | sleep_waits = iter(retry_delays) | 52 | if not any((self._match_ppa(shortcut), |
1389 | 53 | self._match_uri(shortcut), | ||
1390 | 54 | self._match_sourceslist(shortcut))): | ||
1391 | 55 | msg = (_("ERROR: '%s' is not a valid ppa format") % shortcut) | ||
1392 | 56 | raise InvalidShortcutException(msg) | ||
1393 | 163 | 57 | ||
1422 | 164 | while True: | 58 | self._filebase = "%s-ubuntu-%s" % (self.teamname, self.ppaname) |
1423 | 165 | err_msg = None | 59 | self._set_auth() |
1396 | 166 | err = None | ||
1397 | 167 | trynum += 1 | ||
1398 | 168 | try: | ||
1399 | 169 | callback = CurlCallback() | ||
1400 | 170 | curl = pycurl.Curl() | ||
1401 | 171 | curl.setopt(pycurl.SSL_VERIFYPEER, 1) | ||
1402 | 172 | curl.setopt(pycurl.SSL_VERIFYHOST, 2) | ||
1403 | 173 | curl.setopt(pycurl.FOLLOWLOCATION, 1) | ||
1404 | 174 | curl.setopt(pycurl.WRITEFUNCTION, callback.body_callback) | ||
1405 | 175 | if LAUNCHPAD_PPA_CERT: | ||
1406 | 176 | curl.setopt(pycurl.CAINFO, LAUNCHPAD_PPA_CERT) | ||
1407 | 177 | curl.setopt(pycurl.URL, str(lp_url)) | ||
1408 | 178 | if accept_json: | ||
1409 | 179 | curl.setopt(pycurl.HTTPHEADER, ["Accept: application/json"]) | ||
1410 | 180 | curl.perform() | ||
1411 | 181 | response = curl.getinfo(curl.RESPONSE_CODE) | ||
1412 | 182 | curl.close() | ||
1413 | 183 | |||
1414 | 184 | if response != 200: | ||
1415 | 185 | err_msg = "response code %i" % response | ||
1416 | 186 | except pycurl.error as e: | ||
1417 | 187 | err_msg = str(e) | ||
1418 | 188 | err = e | ||
1419 | 189 | |||
1420 | 190 | if err_msg is None: | ||
1421 | 191 | return callback.contents | ||
1424 | 192 | 60 | ||
1489 | 193 | try: | 61 | if not self._source_entry: |
1490 | 194 | time.sleep(next(sleep_waits)) | 62 | uri_format = PRIVATE_PPA_URI_FORMAT if self.lpppa.private else PPA_URI_FORMAT |
1491 | 195 | except StopIteration: | 63 | uri = uri_format.format(team=self.teamname, ppa=self.ppaname) |
1492 | 196 | break | 64 | line = ('%s %s %s %s' % (self.binary_type, uri, self.codename, ' '.join(self.components) or 'main')) |
1493 | 197 | 65 | self._set_source_entry(line) | |
1430 | 198 | raise PPAException( | ||
1431 | 199 | "Error reading %s (%d tries): %s" % (lp_url, trynum, err_msg), | ||
1432 | 200 | original_error=err) | ||
1433 | 201 | |||
1434 | 202 | |||
1435 | 203 | def mangle_ppa_shortcut(shortcut): | ||
1436 | 204 | if ":" in shortcut: | ||
1437 | 205 | ppa_shortcut = shortcut.split(":")[1] | ||
1438 | 206 | else: | ||
1439 | 207 | ppa_shortcut = shortcut | ||
1440 | 208 | if ppa_shortcut.startswith("/"): | ||
1441 | 209 | ppa_shortcut = ppa_shortcut.lstrip("/") | ||
1442 | 210 | user = ppa_shortcut.split("/")[0] | ||
1443 | 211 | if (user[0] == "~"): | ||
1444 | 212 | user = user[1:] | ||
1445 | 213 | ppa_path_objs = ppa_shortcut.split("/")[1:] | ||
1446 | 214 | ppa_path = [] | ||
1447 | 215 | if (len(ppa_path_objs) < 1): | ||
1448 | 216 | ppa_path = ['ubuntu', 'ppa'] | ||
1449 | 217 | elif (len(ppa_path_objs) == 1): | ||
1450 | 218 | ppa_path.insert(0, "ubuntu") | ||
1451 | 219 | ppa_path.extend(ppa_path_objs) | ||
1452 | 220 | else: | ||
1453 | 221 | ppa_path = ppa_path_objs | ||
1454 | 222 | ppa = "~%s/%s" % (user, "/".join(ppa_path)) | ||
1455 | 223 | return ppa | ||
1456 | 224 | |||
1457 | 225 | def verify_keyid_is_v4(signing_key_fingerprint): | ||
1458 | 226 | """Verify that the keyid is a v4 fingerprint with at least 160bit""" | ||
1459 | 227 | return len(signing_key_fingerprint) >= 160/8 | ||
1460 | 228 | |||
1461 | 229 | |||
1462 | 230 | class AddPPASigningKey(object): | ||
1463 | 231 | " thread class for adding the signing key in the background " | ||
1464 | 232 | |||
1465 | 233 | def __init__(self, ppa_path, keyserver=None): | ||
1466 | 234 | self.ppa_path = ppa_path | ||
1467 | 235 | self._homedir = tempfile.mkdtemp() | ||
1468 | 236 | |||
1469 | 237 | def __del__(self): | ||
1470 | 238 | shutil.rmtree(self._homedir) | ||
1471 | 239 | |||
1472 | 240 | def gpg_cmd(self, args): | ||
1473 | 241 | cmd = "gpg -q --homedir %s --no-default-keyring --no-options --import --import-options %s" % (self._homedir, args) | ||
1474 | 242 | return subprocess.Popen(cmd.split(), stdin=subprocess.PIPE, stdout=subprocess.PIPE) | ||
1475 | 243 | |||
1476 | 244 | def _recv_key(self, ppa_info): | ||
1477 | 245 | signing_key_fingerprint = ppa_info["signing_key_fingerprint"] | ||
1478 | 246 | try: | ||
1479 | 247 | # double check that the signing key is a v4 fingerprint (160bit) | ||
1480 | 248 | if not verify_keyid_is_v4(signing_key_fingerprint): | ||
1481 | 249 | print("Error: signing key fingerprint '%s' too short" % | ||
1482 | 250 | signing_key_fingerprint) | ||
1483 | 251 | return False | ||
1484 | 252 | except TypeError: | ||
1485 | 253 | print("Error: signing key fingerprint does not exist") | ||
1486 | 254 | return False | ||
1487 | 255 | |||
1488 | 256 | return get_ppa_signing_key_data(ppa_info) | ||
1494 | 257 | 66 | ||
1523 | 258 | def _minimize_key(self, key): | 67 | @property |
1524 | 259 | p = self.gpg_cmd("import-minimal,import-export") | 68 | def lp(self): |
1525 | 260 | (minimal_key, _) = p.communicate(key.encode()) | 69 | if not self._lp: |
1526 | 261 | 70 | if self._lp_anon: | |
1527 | 262 | if p.returncode != 0: | 71 | login_func = Launchpad.login_anonymously |
1528 | 263 | return False | 72 | else: |
1529 | 264 | return minimal_key | 73 | login_func = Launchpad.login_with |
1530 | 265 | 74 | self._lp = login_func("%s.%s" % (self.__module__, self.__class__.__name__), | |
1531 | 266 | def _get_fingerprints(self, key): | 75 | service_root='production', |
1532 | 267 | fingerprints = [] | 76 | version='devel') |
1533 | 268 | p = self.gpg_cmd("show-only --fingerprint --batch --with-colons") | 77 | return self._lp |
1534 | 269 | (output, _) = p.communicate(key) | 78 | |
1535 | 270 | if p.returncode == 0: | 79 | @property |
1536 | 271 | for line in output.decode('utf-8').splitlines(): | 80 | def lpteam(self): |
1537 | 272 | if line.startswith("fpr:"): | 81 | if not self._lpteam: |
1538 | 273 | fingerprints.append(line.split(":")[9]) | 82 | try: |
1539 | 274 | return fingerprints | 83 | self._lpteam = self.lp.people(self.teamname) |
1540 | 275 | 84 | except NotFound: | |
1541 | 276 | def _verify_fingerprint(self, key, expected_fingerprint): | 85 | msg = (_("ERROR: user/team '%s' not found (use --login if private)") % self.teamname) |
1542 | 277 | got_fingerprints = self._get_fingerprints(key) | 86 | raise ShortcutException(msg) |
1543 | 278 | if len(got_fingerprints) != 1: | 87 | except Unauthorized: |
1544 | 279 | print("Got '%s' fingerprints, expected only one" % | 88 | msg = (_("ERROR: invalid user/team name '%s'") % self.teamname) |
1545 | 280 | len(got_fingerprints)) | 89 | raise ShortcutException(msg) |
1546 | 281 | return False | 90 | return self._lpteam |
1547 | 282 | got_fingerprint = got_fingerprints[0] | 91 | |
1548 | 283 | if got_fingerprint != expected_fingerprint: | 92 | @property |
1549 | 284 | print("Fingerprints do not match, not importing: '%s' != '%s'" % ( | 93 | def lpppa(self): |
1550 | 285 | expected_fingerprint, got_fingerprint)) | 94 | if not self._lpppa: |
1551 | 95 | try: | ||
1552 | 96 | self._lpppa = self.lpteam.getPPAByName(name=self.ppaname) | ||
1553 | 97 | except NotFound: | ||
1554 | 98 | msg = (_("ERROR: ppa '%s/%s' not found (use --login if private)") % | ||
1555 | 99 | (self.teamname, self.ppaname)) | ||
1556 | 100 | raise ShortcutException(msg) | ||
1557 | 101 | except BadRequest: | ||
1558 | 102 | msg = (_("ERROR: invalid ppa name '%s'") % self.ppaname) | ||
1559 | 103 | raise ShortcutException(msg) | ||
1560 | 104 | return self._lpppa | ||
1561 | 105 | |||
1562 | 106 | @property | ||
1563 | 107 | def description(self): | ||
1564 | 108 | return self.lpppa.description | ||
1565 | 109 | |||
1566 | 110 | @property | ||
1567 | 111 | def web_link(self): | ||
1568 | 112 | return self.lpppa.web_link | ||
1569 | 113 | |||
1570 | 114 | @property | ||
1571 | 115 | def trustedparts_content(self): | ||
1572 | 116 | if not self._signing_key_data: | ||
1573 | 117 | key = self.lpppa.getSigningKeyData() | ||
1574 | 118 | fingerprint = self.lpppa.signing_key_fingerprint | ||
1575 | 119 | |||
1576 | 120 | if not fingerprint: | ||
1577 | 121 | msg = _("Warning: could not get PPA signing_key_fingerprint from LP, using anyway") | ||
1578 | 122 | elif not fingerprint in self.fingerprints(key): | ||
1579 | 123 | msg = (_("Fingerprints do not match, not importing: '%s' != '%s'") % | ||
1580 | 124 | (fingerprint, ','.join(self.fingerprints(key)))) | ||
1581 | 125 | raise ShortcutException(msg) | ||
1582 | 126 | |||
1583 | 127 | self._signing_key_data = key | ||
1584 | 128 | return self._signing_key_data | ||
1585 | 129 | |||
1586 | 130 | def _match_ppa(self, shortcut): | ||
1587 | 131 | (prefix, _, ppa) = shortcut.rpartition(':') | ||
1588 | 132 | if not prefix.lower() == 'ppa': | ||
1589 | 286 | return False | 133 | return False |
1590 | 287 | return True | ||
1591 | 288 | 134 | ||
1594 | 289 | def add_ppa_signing_key(self, ppa_path=None): | 135 | (teamname, _, ppaname) = ppa.partition('/') |
1595 | 290 | """Query and add the corresponding PPA signing key. | 136 | teamname = teamname.lstrip('~') |
1596 | 137 | if '/' in ppaname: | ||
1597 | 138 | (ubuntu, _, ppaname) = ppaname.partition('/') | ||
1598 | 139 | if ubuntu.lower() != 'ubuntu': | ||
1599 | 140 | # PPAs only support ubuntu | ||
1600 | 141 | return False | ||
1601 | 142 | if '/' in ppaname: | ||
1602 | 143 | # Path is too long for valid ppa | ||
1603 | 144 | return False | ||
1604 | 291 | 145 | ||
1610 | 292 | The signing key fingerprint is obtained from the Launchpad PPA page, | 146 | self.teamname = teamname |
1611 | 293 | via a secure channel, so it can be trusted. | 147 | self.ppaname = ppaname or 'ppa' |
1612 | 294 | """ | 148 | return True |
1608 | 295 | if ppa_path is None: | ||
1609 | 296 | ppa_path = self.ppa_path | ||
1613 | 297 | 149 | ||
1614 | 150 | def _match_uri(self, shortcut): | ||
1615 | 298 | try: | 151 | try: |
1619 | 299 | ppa_info = get_ppa_info(mangle_ppa_shortcut(ppa_path)) | 152 | return self._match_handler(URIShortcutHandler(shortcut)) |
1620 | 300 | except PPAException as e: | 153 | except InvalidShortcutException: |
1618 | 301 | print(e.value) | ||
1621 | 302 | return False | 154 | return False |
1622 | 155 | |||
1623 | 156 | def _match_sourceslist(self, shortcut): | ||
1624 | 303 | try: | 157 | try: |
1628 | 304 | signing_key_fingerprint = ppa_info["signing_key_fingerprint"] | 158 | return self._match_handler(SourcesListShortcutHandler(shortcut)) |
1629 | 305 | except IndexError: | 159 | except InvalidShortcutException: |
1627 | 306 | print("Error: can't find signing_key_fingerprint at %s" % ppa_path) | ||
1630 | 307 | return False | 160 | return False |
1631 | 308 | |||
1632 | 309 | # download the armored_key | ||
1633 | 310 | armored_key = self._recv_key(ppa_info) | ||
1634 | 311 | if not armored_key: | ||
1635 | 312 | return False | ||
1636 | 313 | |||
1637 | 314 | trustedgpgd = apt_pkg.config.find_dir("Dir::Etc::trustedparts") | ||
1638 | 315 | apt_keyring = os.path.join(trustedgpgd, encode(ppa_info["reference"][1:])) | ||
1639 | 316 | 161 | ||
1642 | 317 | minimal_key = self._minimize_key(armored_key) | 162 | def _match_handler(self, handler): |
1643 | 318 | if not minimal_key: | 163 | parsed = urlparse(handler.SourceEntry().uri) |
1644 | 164 | if not parsed.hostname in PPA_VALID_HOSTNAMES: | ||
1645 | 319 | return False | 165 | return False |
1646 | 320 | |||
1647 | 321 | if not self._verify_fingerprint(minimal_key, signing_key_fingerprint): | ||
1648 | 322 | return False | ||
1649 | 323 | |||
1650 | 324 | with open('%s.gpg' % apt_keyring, 'wb') as f: | ||
1651 | 325 | f.write(minimal_key) | ||
1652 | 326 | |||
1653 | 327 | return True | ||
1654 | 328 | |||
1655 | 329 | 166 | ||
1668 | 330 | class AddPPASigningKeyThread(Thread, AddPPASigningKey): | 167 | path = parsed.path.strip().strip('/').split('/') |
1669 | 331 | # This class is legacy. There are no users inside the software-properties | 168 | if len(path) < 2: |
1670 | 332 | # codebase other than a test case. It was left in case there were outside | 169 | return False |
1671 | 333 | # users. Internally, we've changed from having a class implement the | 170 | self.teamname = path[0] |
1672 | 334 | # tread to explicitly launching a thread and invoking a method in it | 171 | self.ppaname = path[1] |
1661 | 335 | # see check_and_add_key_for_whitelisted_shortcut for how. | ||
1662 | 336 | def __init__(self, ppa_path, keyserver=None): | ||
1663 | 337 | Thread.__init__(self) | ||
1664 | 338 | AddPPASigningKey.__init__(self, ppa_path=ppa_path, keyserver=keyserver) | ||
1665 | 339 | |||
1666 | 340 | def run(self): | ||
1667 | 341 | self.add_ppa_signing_key(self.ppa_path) | ||
1673 | 342 | 172 | ||
1674 | 173 | self._username = handler.username | ||
1675 | 174 | self._password = handler.password | ||
1676 | 343 | 175 | ||
1792 | 344 | def _get_suggested_ppa_message(user, ppa_name): | 176 | self._set_source_entry(handler.SourceEntry().line) |
1678 | 345 | try: | ||
1679 | 346 | msg = [] | ||
1680 | 347 | try: | ||
1681 | 348 | try: | ||
1682 | 349 | lp_user = get_info_from_lp(LAUNCHPAD_USER_API % user) | ||
1683 | 350 | except PPAException: | ||
1684 | 351 | return _("ERROR: '{user}' user or team does not exist.").format(user=user) | ||
1685 | 352 | lp_ppas = get_info_from_lp(LAUNCHPAD_USER_PPAS_API % user) | ||
1686 | 353 | entity_name = _("team") if lp_user["is_team"] else _("user") | ||
1687 | 354 | if lp_ppas["total_size"] > 0: | ||
1688 | 355 | # Translators: %(entity)s is either "team" or "user" | ||
1689 | 356 | msg.append(_("The %(entity)s named '%(user)s' has no PPA named '%(ppa)s'") % { | ||
1690 | 357 | 'entity' : entity_name, | ||
1691 | 358 | 'user' : user, | ||
1692 | 359 | 'ppa' : ppa_name}) | ||
1693 | 360 | msg.append(_("Please choose from the following available PPAs:")) | ||
1694 | 361 | for ppa in lp_ppas["entries"]: | ||
1695 | 362 | msg.append(_(" * '%(name)s': %(displayname)s") % { | ||
1696 | 363 | 'name' : ppa["name"], | ||
1697 | 364 | 'displayname' : ppa["displayname"]}) | ||
1698 | 365 | else: | ||
1699 | 366 | # Translators: %(entity)s is either "team" or "user" | ||
1700 | 367 | msg.append(_("The %(entity)s named '%(user)s' does not have any PPA") % { | ||
1701 | 368 | 'entity' : entity_name, 'user' : user}) | ||
1702 | 369 | return '\n'.join(msg) | ||
1703 | 370 | except KeyError: | ||
1704 | 371 | return '' | ||
1705 | 372 | except ImportError: | ||
1706 | 373 | return _("Please check that the PPA name or format is correct.") | ||
1707 | 374 | |||
1708 | 375 | |||
1709 | 376 | def get_ppa_info(shortcut): | ||
1710 | 377 | user = shortcut.split("/")[0] | ||
1711 | 378 | ppa = "/".join(shortcut.split("/")[1:]) | ||
1712 | 379 | try: | ||
1713 | 380 | ret = get_ppa_info_from_lp(user, ppa) | ||
1714 | 381 | ret["distribution"] = ret["distribution_link"].split('/')[-1] | ||
1715 | 382 | ret["owner"] = ret["owner_link"].split('/')[-1] | ||
1716 | 383 | return ret | ||
1717 | 384 | except (HTTPError, Exception): | ||
1718 | 385 | msg = [] | ||
1719 | 386 | msg.append(_("Cannot add PPA: 'ppa:%s/%s'.") % ( | ||
1720 | 387 | user, ppa)) | ||
1721 | 388 | |||
1722 | 389 | # If the PPA does not exist, then try to find if the user/team | ||
1723 | 390 | # exists. If it exists, list down the PPAs | ||
1724 | 391 | raise ShortcutException('\n'.join(msg) + "\n" + | ||
1725 | 392 | _get_suggested_ppa_message(user, ppa)) | ||
1726 | 393 | |||
1727 | 394 | except (ValueError, PPAException): | ||
1728 | 395 | raise ShortcutException( | ||
1729 | 396 | _("Cannot access PPA (%s) to get PPA information, " | ||
1730 | 397 | "please check your internet connection.") % \ | ||
1731 | 398 | (LAUNCHPAD_PPA_API % (user, ppa))) | ||
1732 | 399 | |||
1733 | 400 | |||
1734 | 401 | def get_ppa_signing_key_data(info=None): | ||
1735 | 402 | """Return signing key data in armored ascii format for the provided ppa. | ||
1736 | 403 | |||
1737 | 404 | If 'info' is a dictionary, it is assumed to be the result | ||
1738 | 405 | of 'get_ppa_info(ppa)'. If it is a string, it is assumed to | ||
1739 | 406 | be a ppa_path. | ||
1740 | 407 | |||
1741 | 408 | Return value is a text string.""" | ||
1742 | 409 | if isinstance(info, dict): | ||
1743 | 410 | link = info["self_link"] | ||
1744 | 411 | else: | ||
1745 | 412 | link = get_ppa_info(mangle_ppa_shortcut(info))["self_link"] | ||
1746 | 413 | |||
1747 | 414 | return get_info_from_https(link + "?ws.op=getSigningKeyData", | ||
1748 | 415 | accept_json=True, retry_delays=(1, 2, 3)) | ||
1749 | 416 | |||
1750 | 417 | |||
1751 | 418 | class PPAShortcutHandler(object): | ||
1752 | 419 | def __init__(self, shortcut): | ||
1753 | 420 | super(PPAShortcutHandler, self).__init__() | ||
1754 | 421 | try: | ||
1755 | 422 | self.shortcut = mangle_ppa_shortcut(shortcut) | ||
1756 | 423 | except: | ||
1757 | 424 | raise ShortcutException(_("ERROR: '{shortcut}' is not a valid ppa format") | ||
1758 | 425 | .format(shortcut=shortcut)) | ||
1759 | 426 | info = get_ppa_info(self.shortcut) | ||
1760 | 427 | |||
1761 | 428 | if "private" in info and info["private"]: | ||
1762 | 429 | raise ShortcutException( | ||
1763 | 430 | _("Adding private PPAs is not supported currently")) | ||
1764 | 431 | |||
1765 | 432 | self._info = info | ||
1766 | 433 | |||
1767 | 434 | def info(self): | ||
1768 | 435 | return self._info | ||
1769 | 436 | |||
1770 | 437 | def expand(self, codename, distro=None): | ||
1771 | 438 | if (distro is not None | ||
1772 | 439 | and distro != self._info["distribution"] | ||
1773 | 440 | and not series_valid_for_distro(self._info["distribution"], codename)): | ||
1774 | 441 | # The requested PPA is for a foreign distribution. Guess that | ||
1775 | 442 | # the user wants that distribution's current series. | ||
1776 | 443 | # This only applies if the local distribution is not the same | ||
1777 | 444 | # distribution the remote PPA is associated with AND the local | ||
1778 | 445 | # codename is not equal to the PPA's series. | ||
1779 | 446 | # e.g. local:Foobar/xenial and ppa:Ubuntu/xenial will use 'xenial' | ||
1780 | 447 | # local:Foobar/fluffy and ppa:Ubuntu/xenial will use '$latest' | ||
1781 | 448 | codename = get_current_series_from_lp(self._info["distribution"]) | ||
1782 | 449 | debline = "deb http://ppa.launchpad.net/%s/%s/%s %s main" % ( | ||
1783 | 450 | self._info["owner"][1:], self._info["name"], | ||
1784 | 451 | self._info["distribution"], codename) | ||
1785 | 452 | sourceslistd = apt_pkg.config.find_dir("Dir::Etc::sourceparts") | ||
1786 | 453 | filename = os.path.join(sourceslistd, "%s-%s-%s-%s.list" % ( | ||
1787 | 454 | encode(self._info["owner"][1:]), encode(self._info["distribution"]), | ||
1788 | 455 | encode(self._info["name"]), codename)) | ||
1789 | 456 | return (debline, filename) | ||
1790 | 457 | |||
1791 | 458 | def should_confirm(self): | ||
1793 | 459 | return True | 177 | return True |
1794 | 460 | 178 | ||
1799 | 461 | def add_key(self, keyserver=None): | 179 | def _set_auth(self): |
1800 | 462 | apsk = AddPPASigningKey(self._info["reference"], keyserver=keyserver) | 180 | if self._lp_anon or not self.lpppa.private: |
1801 | 463 | return apsk.add_ppa_signing_key() | 181 | return |
1798 | 464 | |||
1802 | 465 | 182 | ||
1807 | 466 | def shortcut_handler(shortcut): | 183 | if self._username and self._password: |
1808 | 467 | if not shortcut.startswith("ppa:"): | 184 | return |
1805 | 468 | return None | ||
1806 | 469 | return PPAShortcutHandler(shortcut) | ||
1809 | 470 | 185 | ||
1815 | 471 | 186 | for url in self.lp.me.getArchiveSubscriptionURLs(): | |
1816 | 472 | if __name__ == "__main__": | 187 | parsed = urlparse(url) |
1817 | 473 | import sys | 188 | if parsed.path.startswith(f'/{self.teamname}/{self.ppaname}/ubuntu'): |
1818 | 474 | ppa = mangle_ppa_shortcut(sys.argv[1]) | 189 | self._username = parsed.username |
1819 | 475 | print(get_ppa_info(ppa)) | 190 | self._password = parsed.password |
1820 | 191 | break | ||
1821 | 192 | else: | ||
1822 | 193 | msg = (_("Could not find PPA subscription for ppa:%s/%s, you may need to request access") % | ||
1823 | 194 | (self.teamname, self.ppaname)) | ||
1824 | 195 | raise ShortcutException(msg) | ||
1825 | diff --git a/softwareproperties/shortcuthandler.py b/softwareproperties/shortcuthandler.py | |||
1826 | 476 | new file mode 100644 | 196 | new file mode 100644 |
1827 | index 0000000..6057e13 | |||
1828 | --- /dev/null | |||
1829 | +++ b/softwareproperties/shortcuthandler.py | |||
1830 | @@ -0,0 +1,617 @@ | |||
1831 | 1 | # Copyright (c) 2019 Canonical Ltd. | ||
1832 | 2 | # | ||
1833 | 3 | # This program is free software; you can redistribute it and/or | ||
1834 | 4 | # modify it under the terms of the GNU General Public License as | ||
1835 | 5 | # published by the Free Software Foundation; either version 2 of the | ||
1836 | 6 | # License, or (at your option) any later version. | ||
1837 | 7 | # | ||
1838 | 8 | # This program is distributed in the hope that it will be useful, | ||
1839 | 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1840 | 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1841 | 11 | # GNU General Public License for more details. | ||
1842 | 12 | # | ||
1843 | 13 | # You should have received a copy of the GNU General Public License | ||
1844 | 14 | # along with this program; if not, write to the Free Software | ||
1845 | 15 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 | ||
1846 | 16 | # USA | ||
1847 | 17 | |||
1848 | 18 | import os | ||
1849 | 19 | import re | ||
1850 | 20 | import apt_pkg | ||
1851 | 21 | import subprocess | ||
1852 | 22 | import tempfile | ||
1853 | 23 | |||
1854 | 24 | from aptsources.distro import get_distro | ||
1855 | 25 | from aptsources.sourceslist import (SourceEntry, SourcesList) | ||
1856 | 26 | |||
1857 | 27 | from contextlib import suppress | ||
1858 | 28 | |||
1859 | 29 | from copy import copy | ||
1860 | 30 | |||
1861 | 31 | from gettext import gettext as _ | ||
1862 | 32 | |||
1863 | 33 | from urllib.parse import urlparse | ||
1864 | 34 | |||
1865 | 35 | |||
1866 | 36 | GPG_KEYRING_CMD = 'gpg -q --no-options --no-default-keyring --batch --keyring %s' | ||
1867 | 37 | |||
1868 | 38 | class ShortcutHandler(object): | ||
1869 | 39 | '''Superclass for shortcut handler implementations. | ||
1870 | 40 | |||
1871 | 41 | This provides a way to take a apt repository reference, in various forms, | ||
1872 | 42 | and write the specific apt configuration to local files. This also can | ||
1873 | 43 | remove previously written configuration from local files. | ||
1874 | 44 | |||
1875 | 45 | This class and any subclasses should never modify any main apt configuration | ||
1876 | 46 | files, only specifically named files in '.d' subdirs (e.g. sources.list.d, etc) | ||
1877 | 47 | should be modified. The only exception to that rule is adding or removing | ||
1878 | 48 | sourceslist lines or components of existing source entries. | ||
1879 | 49 | ''' | ||
1880 | 50 | def __init__(self, shortcut, components=None, enable_source=False, codename=None, dry_run=False, **kwargs): | ||
1881 | 51 | self.shortcut = shortcut | ||
1882 | 52 | self.components = components or [] | ||
1883 | 53 | self.enable_source = enable_source | ||
1884 | 54 | self.distro = get_distro() | ||
1885 | 55 | self.codename = codename or self.distro.codename | ||
1886 | 56 | self.dry_run = dry_run | ||
1887 | 57 | |||
1888 | 58 | # Subclasses should not directly reference _source_entry, | ||
1889 | 59 | # use _set_source_entry() and SourceEntry() | ||
1890 | 60 | self._source_entry = None | ||
1891 | 61 | |||
1892 | 62 | # Subclasses should directly set these fields, if appropriate | ||
1893 | 63 | self._filebase = None | ||
1894 | 64 | self._username = None | ||
1895 | 65 | self._password = None | ||
1896 | 66 | |||
1897 | 67 | @classmethod | ||
1898 | 68 | def is_valid_uri(cls, uri): | ||
1899 | 69 | '''Return if the uri is in valid uri format''' | ||
1900 | 70 | parsed = urlparse(uri) | ||
1901 | 71 | return parsed.scheme and parsed.netloc | ||
1902 | 72 | |||
1903 | 73 | @classmethod | ||
1904 | 74 | def uri_strip_auth(cls, uri): | ||
1905 | 75 | '''Return the uri with the username and password stripped''' | ||
1906 | 76 | parsed = urlparse(uri) | ||
1907 | 77 | # urlparse doesn't have any great way to simply remove the auth data, | ||
1908 | 78 | # so let's just strip everything to the left of '@' | ||
1909 | 79 | return parsed._replace(netloc=parsed.netloc.rpartition('@')[2]).geturl() | ||
1910 | 80 | |||
1911 | 81 | @classmethod | ||
1912 | 82 | def uri_insert_auth(cls, uri, username, password): | ||
1913 | 83 | '''Return the uri with the username and password included''' | ||
1914 | 84 | parsed = urlparse(cls.uri_strip_auth(uri)) | ||
1915 | 85 | netloc='%s:%s@%s' % (username, password, parsed.netloc) | ||
1916 | 86 | return parsed._replace(netloc=netloc).geturl() | ||
1917 | 87 | |||
1918 | 88 | @classmethod | ||
1919 | 89 | def fingerprints(cls, keys): | ||
1920 | 90 | '''Return an array of fingerprint(s) for provided key(s). | ||
1921 | 91 | |||
1922 | 92 | The 'keys' parameter should be in text (str) or binary (bytes) format; | ||
1923 | 93 | it is converted to bytes if needed, and then passed to the 'gpg' program. | ||
1924 | 94 | ''' | ||
1925 | 95 | cmd = 'gpg -q --no-options --no-keyring --batch --with-colons' | ||
1926 | 96 | # yes, --with-fingerprint twice, to print subkey fingerprints | ||
1927 | 97 | cmd += ' --with-fingerprint' * 2 | ||
1928 | 98 | try: | ||
1929 | 99 | with tempfile.TemporaryDirectory() as homedir: | ||
1930 | 100 | cmd += f' --homedir {homedir}' | ||
1931 | 101 | if not isinstance(keys, bytes): | ||
1932 | 102 | keys = keys.encode() | ||
1933 | 103 | stdout = subprocess.run(cmd.split(), check=True, input=keys, | ||
1934 | 104 | stdout=subprocess.PIPE).stdout.decode() | ||
1935 | 105 | except subprocess.CalledProcessError as e: | ||
1936 | 106 | print(_("Warning: gpg error while processing keys:\n%s") % e) | ||
1937 | 107 | return [] | ||
1938 | 108 | |||
1939 | 109 | try: | ||
1940 | 110 | # gpg --with-colons fpr field puts fingerprint into (1-based) field 10 | ||
1941 | 111 | return [l.split(':')[9] for l in stdout.splitlines() if l.startswith('fpr')] | ||
1942 | 112 | except KeyError: | ||
1943 | 113 | print(_("Warning: invalid gpg output:\n%s") % stdout) | ||
1944 | 114 | return [] | ||
1945 | 115 | |||
1946 | 116 | @property | ||
1947 | 117 | def description(self): | ||
1948 | 118 | return (_("Archive for codename: %s components: %s" % | ||
1949 | 119 | (self.SourceEntry().dist, | ||
1950 | 120 | ','.join(self.SourceEntry().comps)))) | ||
1951 | 121 | |||
1952 | 122 | @property | ||
1953 | 123 | def web_link(self): | ||
1954 | 124 | return self.archive_link | ||
1955 | 125 | |||
1956 | 126 | @property | ||
1957 | 127 | def archive_link(self): | ||
1958 | 128 | return self.SourceEntry().uri | ||
1959 | 129 | |||
1960 | 130 | @property | ||
1961 | 131 | def binary_type(self): | ||
1962 | 132 | '''Text indicating a binary-type SourceEntry.''' | ||
1963 | 133 | return self.distro.binary_type | ||
1964 | 134 | |||
1965 | 135 | @property | ||
1966 | 136 | def source_type(self): | ||
1967 | 137 | '''Text indicating a source-type SourceEntry.''' | ||
1968 | 138 | return self.distro.source_type | ||
1969 | 139 | |||
1970 | 140 | def SourceEntry(self, pkgtype=None): | ||
1971 | 141 | '''Get the SourceEntry representing this archive/shortcut. | ||
1972 | 142 | |||
1973 | 143 | This should never include any authentication data; if required, | ||
1974 | 144 | the username and password should only be available from the | ||
1975 | 145 | username and password properties, as well as from the | ||
1976 | 146 | netrcparts_content property. | ||
1977 | 147 | |||
1978 | 148 | If pkgtype is provided, it must be either binary_type or source_type, | ||
1979 | 149 | in which case this returns a SourceEntry with the requested type. | ||
1980 | 150 | If pkgtype is not specified, this returns a SourceEntry with an | ||
1981 | 151 | implementation-dependent type (in most cases, implementations should | ||
1982 | 152 | default to binary_type). | ||
1983 | 153 | |||
1984 | 154 | Note that the default SourceEntry will be returned without modification, | ||
1985 | 155 | and the implementation will determine if it is enabled or disabled; | ||
1986 | 156 | while the source-type SourceEntry will be enabled or disabled based on | ||
1987 | 157 | self.enable_source. The binary-type SourceEntry will always be enabled. | ||
1988 | 158 | |||
1989 | 159 | The SourceEntry 'file' field should always be set to the value of | ||
1990 | 160 | sourceparts_file. | ||
1991 | 161 | ''' | ||
1992 | 162 | if not self._source_entry: | ||
1993 | 163 | raise NotImplementedError('Implementation class did not set self._source_entry') | ||
1994 | 164 | e = copy(self._source_entry) | ||
1995 | 165 | if not pkgtype: | ||
1996 | 166 | return e | ||
1997 | 167 | if pkgtype == self.binary_type: | ||
1998 | 168 | e.set_enabled(True) | ||
1999 | 169 | e.type = self.binary_type | ||
2000 | 170 | elif pkgtype == self.source_type: | ||
2001 | 171 | e.set_enabled(self.enable_source) | ||
2002 | 172 | e.type = self.source_type | ||
2003 | 173 | else: | ||
2004 | 174 | raise ValueError('Invalid pkgtype: %s' % pkgtype) | ||
2005 | 175 | return SourceEntry(str(e), file=e.file) | ||
2006 | 176 | |||
2007 | 177 | @property | ||
2008 | 178 | def username(self): | ||
2009 | 179 | '''Return the username used for authentication | ||
2010 | 180 | |||
2011 | 181 | If authentication is used, return the username; otherwise return None. | ||
2012 | 182 | |||
2013 | 183 | By default, this returns the private variable self._username, which | ||
2014 | 184 | defaults to None. Subclasses should override this method and/or | ||
2015 | 185 | set self._username if they have authentication data. | ||
2016 | 186 | ''' | ||
2017 | 187 | return self._username | ||
2018 | 188 | |||
2019 | 189 | @property | ||
2020 | 190 | def password(self): | ||
2021 | 191 | '''Return the password used for authentication | ||
2022 | 192 | |||
2023 | 193 | If authentication is used, return the password; otherwise return None. | ||
2024 | 194 | |||
2025 | 195 | By default, this returns the private variable self._password, which | ||
2026 | 196 | defaults to None. Subclasses should override this method and/or | ||
2027 | 197 | set self._password if they have authentication data. | ||
2028 | 198 | ''' | ||
2029 | 199 | return self._password | ||
2030 | 200 | |||
2031 | 201 | def add(self): | ||
2032 | 202 | '''Save all data for this shortcut to file(s). | ||
2033 | 203 | |||
2034 | 204 | This writes everything to the relevant files. By default, it | ||
2035 | 205 | calls add_source(), add_key(), and add_login(). Subclasses | ||
2036 | 206 | should override it if other actions are required. | ||
2037 | 207 | ''' | ||
2038 | 208 | self.add_source() | ||
2039 | 209 | self.add_key() | ||
2040 | 210 | self.add_login() | ||
2041 | 211 | |||
2042 | 212 | def remove(self): | ||
2043 | 213 | '''Remove all data for this shortcut from file(s). | ||
2044 | 214 | |||
2045 | 215 | This removes everything from the relevant files. By default, it | ||
2046 | 216 | only calls remove_source() and remove_login(). Subclasses | ||
2047 | 217 | should override it if other actions are required. Note that by | ||
2048 | 218 | default is does not call remove_key(). | ||
2049 | 219 | ''' | ||
2050 | 220 | self.remove_source() | ||
2051 | 221 | self.remove_login() | ||
2052 | 222 | |||
2053 | 223 | def add_source(self): | ||
2054 | 224 | '''Add the apt SourceEntries. | ||
2055 | 225 | |||
2056 | 226 | This uses SourcesList to add the binary-type and source-type | ||
2057 | 227 | SourceEntries. | ||
2058 | 228 | |||
2059 | 229 | If the SourceEntry matches a known apt template, this will ignore | ||
2060 | 230 | the sourceparts_file and instead place the SourceEntries into | ||
2061 | 231 | the main/default sources.list file. Otherwise, this will add | ||
2062 | 232 | the SourceEntries into the sourceparts_file. | ||
2063 | 233 | |||
2064 | 234 | If either the binary-type or source-type entry exist in the current | ||
2065 | 235 | SourcesList, the existing entries are updated instead of placing | ||
2066 | 236 | the entries in the sourceparts_file. | ||
2067 | 237 | ''' | ||
2068 | 238 | binentry = self.SourceEntry(self.binary_type) | ||
2069 | 239 | srcentry = self.SourceEntry(self.source_type) | ||
2070 | 240 | mode = self.sourceparts_mode | ||
2071 | 241 | |||
2072 | 242 | sourceslist = SourcesList() | ||
2073 | 243 | |||
2074 | 244 | count = len(sourceslist.list) | ||
2075 | 245 | newentry = sourceslist.add_entry(binentry) | ||
2076 | 246 | if count == len(sourceslist.list): | ||
2077 | 247 | print(_("Found existing %s entry in %s") % (newentry.type, newentry.file)) | ||
2078 | 248 | if binentry.file != newentry.file: | ||
2079 | 249 | # existing binentry, but not in file we were expecting, just update it | ||
2080 | 250 | print(_("Updating existing entry instead of using %s") % binentry.file) | ||
2081 | 251 | elif newentry.template: | ||
2082 | 252 | # our SourceEntry matches a template; use default sources.list file | ||
2083 | 253 | newentry.file = SourceEntry('').file | ||
2084 | 254 | print(_("Archive has template, updating %s") % newentry.file) | ||
2085 | 255 | elif binentry.disabled: | ||
2086 | 256 | print(_("Adding disabled %s entry to %s") % (newentry.type, newentry.file)) | ||
2087 | 257 | else: | ||
2088 | 258 | print(_("Adding %s entry to %s") % (newentry.type, newentry.file)) | ||
2089 | 259 | binpos = sourceslist.list.index(newentry) | ||
2090 | 260 | newentry.set_enabled(not binentry.disabled) | ||
2091 | 261 | binentry = newentry | ||
2092 | 262 | |||
2093 | 263 | # Unless it already exists somewhere, add the srcentry right after the binentry | ||
2094 | 264 | srcentry.file = binentry.file | ||
2095 | 265 | count = len(sourceslist.list) | ||
2096 | 266 | newentry = sourceslist.add_entry(srcentry, pos=binpos) | ||
2097 | 267 | if count == len(sourceslist.list): | ||
2098 | 268 | print(_("Found existing %s entry in %s") % (newentry.type, newentry.file)) | ||
2099 | 269 | if srcentry.file != newentry.file: | ||
2100 | 270 | # existing srcentry, but not in file we were expecting, just update it | ||
2101 | 271 | print(_("Updating existing entry instead of using %s") % srcentry.file) | ||
2102 | 272 | elif srcentry.disabled: | ||
2103 | 273 | print(_("Adding disabled %s entry to %s") % (newentry.type, newentry.file)) | ||
2104 | 274 | else: | ||
2105 | 275 | print(_("Adding %s entry to %s") % (newentry.type, newentry.file)) | ||
2106 | 276 | newentry.set_enabled(not srcentry.disabled) | ||
2107 | 277 | srcentry = newentry | ||
2108 | 278 | |||
2109 | 279 | if not self.dry_run: | ||
2110 | 280 | # If the file doesn't exist, create it so we can set the mode | ||
2111 | 281 | for entryfile in set([binentry.file, srcentry.file]): | ||
2112 | 282 | if not os.path.exists(entryfile): | ||
2113 | 283 | with open(entryfile, 'w'): | ||
2114 | 284 | os.chmod(entryfile, mode) | ||
2115 | 285 | sourceslist.save() | ||
2116 | 286 | |||
2117 | 287 | def remove_source(self): | ||
2118 | 288 | '''Remove the apt SourceEntries. | ||
2119 | 289 | |||
2120 | 290 | This uses SourcesList to remove the binary-type and source-type | ||
2121 | 291 | SourceEntries. | ||
2122 | 292 | |||
2123 | 293 | This must disable the corresponding SourceEntries, from whatever file(s) | ||
2124 | 294 | they are located in. This must not disable more than matches, e.g. | ||
2125 | 295 | if the existing SourceEntry line contains more components this must | ||
2126 | 296 | edit the existing line to remove this SourceEntry's component(s). | ||
2127 | 297 | |||
2128 | 298 | After disabling all matching SourceEntries, if the sourceparts_file is | ||
2129 | 299 | empty or contains only invalid and/or disabled SourceEntries, this | ||
2130 | 300 | may remove the sourceparts_file. | ||
2131 | 301 | ''' | ||
2132 | 302 | sourceslist = SourcesList() | ||
2133 | 303 | |||
2134 | 304 | binentry = self.SourceEntry(self.binary_type) | ||
2135 | 305 | srcentry = self.SourceEntry(self.source_type) | ||
2136 | 306 | binentry.set_enabled(False) | ||
2137 | 307 | srcentry.set_enabled(False) | ||
2138 | 308 | |||
2139 | 309 | # first, disable our entries | ||
2140 | 310 | print(_("Disabling %s entry in %s") % (binentry.type, binentry.file)) | ||
2141 | 311 | sourceslist.add_entry(binentry) | ||
2142 | 312 | print(_("Disabling %s entry in %s") % (srcentry.type, srcentry.file)) | ||
2143 | 313 | sourceslist.add_entry(srcentry) | ||
2144 | 314 | |||
2145 | 315 | file_entries = [s for s in sourceslist if s.file == self.sourceparts_file] | ||
2146 | 316 | if not [e for e in file_entries if not e.invalid and not e.disabled]: | ||
2147 | 317 | # no more valid/enabled entries in our file, remove them | ||
2148 | 318 | for e in file_entries: | ||
2149 | 319 | if not e.invalid: | ||
2150 | 320 | print(_("Removing disabled %s entry from %s") % (e.type, e.file)) | ||
2151 | 321 | sourceslist.remove(e) | ||
2152 | 322 | |||
2153 | 323 | if not self.dry_run: | ||
2154 | 324 | sourceslist.save(remove=True) | ||
2155 | 325 | |||
2156 | 326 | @property | ||
2157 | 327 | def sourceparts_path(self): | ||
2158 | 328 | '''Return result of apt_pkg.config.find_dir("Dir::Etc::sourceparts")''' | ||
2159 | 329 | return apt_pkg.config.find_dir("Dir::Etc::sourceparts") | ||
2160 | 330 | |||
2161 | 331 | @property | ||
2162 | 332 | def sourceparts_filename(self): | ||
2163 | 333 | '''Get the sources.list.d filename, without the leading path. | ||
2164 | 334 | |||
2165 | 335 | By default, this combines the filebase with the codename, and uses a | ||
2166 | 336 | extension of 'list'. This is different than the trustedparts or | ||
2167 | 337 | netrcparts filenames, which use only the filebase plus extension. | ||
2168 | 338 | ''' | ||
2169 | 339 | return self._filebase_to_filename('list', suffix=self.codename) | ||
2170 | 340 | |||
2171 | 341 | @property | ||
2172 | 342 | def sourceparts_file(self): | ||
2173 | 343 | '''Get the sources.list.d absolute-path filename. | ||
2174 | 344 | |||
2175 | 345 | Note that the add_source() function will not use this file if this shortcut's | ||
2176 | 346 | SourceEntry matches a known apt template; instead the entries will be placed | ||
2177 | 347 | in the main sources.list file. Also, if the SourceEntry already exists in | ||
2178 | 348 | the SourcesList, it will be edited in place, instead of using this file. | ||
2179 | 349 | See add_source() for more details. | ||
2180 | 350 | ''' | ||
2181 | 351 | return self._filename_to_file(self.sourceparts_path, self.sourceparts_filename) | ||
2182 | 352 | |||
2183 | 353 | @property | ||
2184 | 354 | def sourceparts_mode(self): | ||
2185 | 355 | '''Mode of sourceparts file. | ||
2186 | 356 | |||
2187 | 357 | Note that add_source() will only use this mode if it creates a new file | ||
2188 | 358 | for sourceparts_file; if the file already exists or if the SourceEntry is | ||
2189 | 359 | saved in a different file, this mode is not used. | ||
2190 | 360 | ''' | ||
2191 | 361 | return 0o644 | ||
2192 | 362 | |||
2193 | 363 | def add_key(self): | ||
2194 | 364 | '''Add the GPG key(s) corresponding to this repo. | ||
2195 | 365 | |||
2196 | 366 | By default, if self.trustedparts_content contains content, | ||
2197 | 367 | and self.trustedparts_file points to a file, the key(s) will | ||
2198 | 368 | be added to the file. | ||
2199 | 369 | |||
2200 | 370 | If the file does not yet exist, and self.trustedparts_mode is set, | ||
2201 | 371 | the file will be created with that mode. | ||
2202 | 372 | ''' | ||
2203 | 373 | if not all((self.trustedparts_file, self.trustedparts_content)): | ||
2204 | 374 | return | ||
2205 | 375 | |||
2206 | 376 | dest = self.trustedparts_file | ||
2207 | 377 | keys = self.trustedparts_content | ||
2208 | 378 | if not isinstance(keys, bytes): | ||
2209 | 379 | keys = keys.encode() | ||
2210 | 380 | fp = self.fingerprints(keys) | ||
2211 | 381 | |||
2212 | 382 | print(_("Adding key to %s with fingerprint %s") % (dest, ','.join(fp))) | ||
2213 | 383 | |||
2214 | 384 | cmd = GPG_KEYRING_CMD % dest | ||
2215 | 385 | action = "--import" | ||
2216 | 386 | if not self.dry_run: | ||
2217 | 387 | if not os.path.exists(dest) and self.trustedparts_mode: | ||
2218 | 388 | with open(dest, 'wb'): | ||
2219 | 389 | os.chmod(dest, self.trustedparts_mode) | ||
2220 | 390 | try: | ||
2221 | 391 | with tempfile.TemporaryDirectory() as homedir: | ||
2222 | 392 | cmd += f" --homedir {homedir} {action}" | ||
2223 | 393 | subprocess.run(cmd.split(), check=True, input=keys) | ||
2224 | 394 | except subprocess.CalledProcessError as e: | ||
2225 | 395 | raise ShortcutException(e) | ||
2226 | 396 | |||
2227 | 397 | def remove_key(self): | ||
2228 | 398 | '''Remove the GPG key(s) corresponding to this repo. | ||
2229 | 399 | |||
2230 | 400 | By default, if self.trustedparts_content contains content, | ||
2231 | 401 | and self.trustedparts_file points to a file, the key(s) will | ||
2232 | 402 | be removed from the file. | ||
2233 | 403 | |||
2234 | 404 | If the file contains no more keys after removal, the file will | ||
2235 | 405 | be removed. | ||
2236 | 406 | |||
2237 | 407 | This does not consider other files; multiple repositories may | ||
2238 | 408 | use the same signing key. This only modifies/removes | ||
2239 | 409 | self.trustedparts_file. | ||
2240 | 410 | ''' | ||
2241 | 411 | if not all((self.trustedparts_file, self.trustedparts_content)): | ||
2242 | 412 | return | ||
2243 | 413 | |||
2244 | 414 | dest = self.trustedparts_file | ||
2245 | 415 | fp = self.fingerprints(self.trustedparts_content) | ||
2246 | 416 | |||
2247 | 417 | if not os.path.exists(dest): | ||
2248 | 418 | return | ||
2249 | 419 | |||
2250 | 420 | print(_("Removing key from %s with fingerprint %s") % (dest, ','.join(fp))) | ||
2251 | 421 | |||
2252 | 422 | cmd = GPG_KEYRING_CMD % dest | ||
2253 | 423 | action = "--delete-keys %s" % ' '.join(fp) | ||
2254 | 424 | if not self.dry_run: | ||
2255 | 425 | try: | ||
2256 | 426 | with tempfile.TemporaryDirectory() as homedir: | ||
2257 | 427 | cmd += f" --homedir {homedir} {action}" | ||
2258 | 428 | subprocess.run(cmd.split(), check=True) | ||
2259 | 429 | except subprocess.CalledProcessError as e: | ||
2260 | 430 | raise ShortcutException(e) | ||
2261 | 431 | |||
2262 | 432 | with open(dest, 'rb') as f: | ||
2263 | 433 | empty = not self.fingerprints(f.read()) | ||
2264 | 434 | if empty: | ||
2265 | 435 | os.remove(dest) | ||
2266 | 436 | |||
2267 | 437 | @property | ||
2268 | 438 | def trustedparts_path(self): | ||
2269 | 439 | '''Return result of apt_pkg.config.find_dir("Dir::Etc::trustedparts")''' | ||
2270 | 440 | return apt_pkg.config.find_dir("Dir::Etc::trustedparts") | ||
2271 | 441 | |||
2272 | 442 | @property | ||
2273 | 443 | def trustedparts_filename(self): | ||
2274 | 444 | '''Get the trusted.gpg.d filename, without the leading path.''' | ||
2275 | 445 | return self._filebase_to_filename('gpg') | ||
2276 | 446 | |||
2277 | 447 | @property | ||
2278 | 448 | def trustedparts_file(self): | ||
2279 | 449 | '''Get the trusted.gpg.d absolute-path filename.''' | ||
2280 | 450 | return self._filename_to_file(self.trustedparts_path, self.trustedparts_filename) | ||
2281 | 451 | |||
2282 | 452 | @property | ||
2283 | 453 | def trustedparts_content(self): | ||
2284 | 454 | '''Content to put into trusted.gpg.d file''' | ||
2285 | 455 | return None | ||
2286 | 456 | |||
2287 | 457 | @property | ||
2288 | 458 | def trustedparts_mode(self): | ||
2289 | 459 | '''Mode of trustedparts file''' | ||
2290 | 460 | return 0o644 | ||
2291 | 461 | |||
2292 | 462 | def add_login(self): | ||
2293 | 463 | '''Add the login credentials corresponding to this repo. | ||
2294 | 464 | |||
2295 | 465 | By default, if self.netrcparts_content contains content, | ||
2296 | 466 | and self.netrcparts_file points to a file, the file will be | ||
2297 | 467 | created and content placed into it. | ||
2298 | 468 | ''' | ||
2299 | 469 | if not all((self.netrcparts_file, self.netrcparts_content)): | ||
2300 | 470 | return | ||
2301 | 471 | |||
2302 | 472 | dest = self.netrcparts_file | ||
2303 | 473 | content = self.netrcparts_content | ||
2304 | 474 | |||
2305 | 475 | newfile = not os.path.exists(dest) | ||
2306 | 476 | finalchar = '\n' | ||
2307 | 477 | if not newfile: | ||
2308 | 478 | with open(dest, 'r') as f: | ||
2309 | 479 | lines = [l.strip() for l in f.readlines()] | ||
2310 | 480 | with suppress(KeyError): | ||
2311 | 481 | finalchar = lines[-1][-1] | ||
2312 | 482 | if all([l.strip() in lines for l in content.splitlines()]): | ||
2313 | 483 | print(_("Authentication data already in %s") % dest) | ||
2314 | 484 | return | ||
2315 | 485 | |||
2316 | 486 | print(_("Adding authentication data to %s") % dest) | ||
2317 | 487 | if not self.dry_run: | ||
2318 | 488 | if newfile and self.netrcparts_mode: | ||
2319 | 489 | with open(dest, 'w'): | ||
2320 | 490 | os.chmod(dest, self.netrcparts_mode) | ||
2321 | 491 | with open(dest, 'a') as f: | ||
2322 | 492 | # we're appending; if the file doesn't end in \n, throw one in | ||
2323 | 493 | if finalchar != '\n': | ||
2324 | 494 | f.write('\n') | ||
2325 | 495 | f.write(self.netrcparts_content) | ||
2326 | 496 | |||
2327 | 497 | def remove_login(self): | ||
2328 | 498 | '''Remove the login credentials corresponding to this repo. | ||
2329 | 499 | |||
2330 | 500 | By default, if self.netrcparts_content contains content, | ||
2331 | 501 | and self.netrcparts_file points to a file, the content will | ||
2332 | 502 | be removed from the file. | ||
2333 | 503 | |||
2334 | 504 | If the file is empty (other than whitespace) after removal, the file | ||
2335 | 505 | will be removed. | ||
2336 | 506 | |||
2337 | 507 | This does not consider other files; this only modifies/removes | ||
2338 | 508 | self.netrcparts_file. | ||
2339 | 509 | ''' | ||
2340 | 510 | if not all((self.netrcparts_file, self.netrcparts_content)): | ||
2341 | 511 | return | ||
2342 | 512 | |||
2343 | 513 | dest = self.netrcparts_file | ||
2344 | 514 | content = set([l.strip() for l in self.netrcparts_content.splitlines()]) | ||
2345 | 515 | |||
2346 | 516 | if not os.path.exists(dest): | ||
2347 | 517 | return | ||
2348 | 518 | |||
2349 | 519 | with open(dest, 'r') as f: | ||
2350 | 520 | filecontent = set([l.strip() for l in f.readlines()]) | ||
2351 | 521 | if not filecontent & content: | ||
2352 | 522 | print(_("Authentication data not contained in %s") % dest) | ||
2353 | 523 | else: | ||
2354 | 524 | print(_("Removing authentication data from %s") % dest) | ||
2355 | 525 | if not self.dry_run: | ||
2356 | 526 | with open(dest, 'w') as f: | ||
2357 | 527 | f.write('\n'.join(filecontent - content)) | ||
2358 | 528 | |||
2359 | 529 | if not self.dry_run: | ||
2360 | 530 | with open(dest, 'r') as f: | ||
2361 | 531 | empty = not f.read().strip() | ||
2362 | 532 | if empty: | ||
2363 | 533 | os.remove(dest) | ||
2364 | 534 | |||
2365 | 535 | @property | ||
2366 | 536 | def netrcparts_path(self): | ||
2367 | 537 | '''Return result of apt_pkg.config.find_dir("Dir::Etc::netrcparts")''' | ||
2368 | 538 | return apt_pkg.config.find_dir("Dir::Etc::netrcparts") | ||
2369 | 539 | |||
2370 | 540 | @property | ||
2371 | 541 | def netrcparts_filename(self): | ||
2372 | 542 | '''Get the auth.conf.d filename, without the leading path.''' | ||
2373 | 543 | return self._filebase_to_filename('conf') | ||
2374 | 544 | |||
2375 | 545 | @property | ||
2376 | 546 | def netrcparts_file(self): | ||
2377 | 547 | '''Get the auth.conf.d absolute-path filename.''' | ||
2378 | 548 | return self._filename_to_file(self.netrcparts_path, self.netrcparts_filename) | ||
2379 | 549 | |||
2380 | 550 | @property | ||
2381 | 551 | def netrcparts_content(self): | ||
2382 | 552 | '''Content to put into auth.conf.d file | ||
2383 | 553 | |||
2384 | 554 | By default, if both username and password are set, this will return a proper | ||
2385 | 555 | netrc-formatted line with the authentication information, including the | ||
2386 | 556 | hostname and path. | ||
2387 | 557 | ''' | ||
2388 | 558 | if not all((self.username, self.password)): | ||
2389 | 559 | return None | ||
2390 | 560 | |||
2391 | 561 | hostname = urlparse(self.SourceEntry().uri).hostname | ||
2392 | 562 | path = urlparse(self.SourceEntry().uri).path | ||
2393 | 563 | return f'machine {hostname}{path} login {self.username} password {self.password}' | ||
2394 | 564 | |||
2395 | 565 | @property | ||
2396 | 566 | def netrcparts_mode(self): | ||
2397 | 567 | '''Mode of netrcparts file''' | ||
2398 | 568 | return 0o600 | ||
2399 | 569 | |||
2400 | 570 | def _set_source_entry(self, line): | ||
2401 | 571 | '''Set the SourceEntry. | ||
2402 | 572 | |||
2403 | 573 | This should be called from subclasses to set the SourceEntry. | ||
2404 | 574 | The SourceEntry file will be set to the sourceparts_file value. | ||
2405 | 575 | |||
2406 | 576 | The self.components, if any, will be added to the line's component(s). | ||
2407 | 577 | ''' | ||
2408 | 578 | e = SourceEntry(line) | ||
2409 | 579 | e.comps = list(set(e.comps) | set(self.components)) | ||
2410 | 580 | self._source_entry = SourceEntry(str(e), file=self.sourceparts_file) | ||
2411 | 581 | |||
2412 | 582 | def _encode_filebase(self, suffix=None): | ||
2413 | 583 | base = self._filebase | ||
2414 | 584 | if not base: | ||
2415 | 585 | return None | ||
2416 | 586 | if suffix: | ||
2417 | 587 | base += '-%s' % suffix | ||
2418 | 588 | return re.sub("[^a-z0-9_-]+", "_", base.lower()) | ||
2419 | 589 | |||
2420 | 590 | def _filebase_to_filename(self, ext, suffix=None): | ||
2421 | 591 | base = self._encode_filebase(suffix=suffix) | ||
2422 | 592 | if not base: | ||
2423 | 593 | return None | ||
2424 | 594 | return '%s.%s' % (base, ext) | ||
2425 | 595 | |||
2426 | 596 | def _filename_to_file(self, path, name): | ||
2427 | 597 | if not name: | ||
2428 | 598 | return None | ||
2429 | 599 | return os.path.join(path, name) | ||
2430 | 600 | |||
2431 | 601 | |||
2432 | 602 | class ShortcutException(Exception): | ||
2433 | 603 | '''General Exception during shortcut processing.''' | ||
2434 | 604 | pass | ||
2435 | 605 | |||
2436 | 606 | |||
2437 | 607 | class InvalidShortcutException(ShortcutException): | ||
2438 | 608 | '''Invalid shortcut. | ||
2439 | 609 | |||
2440 | 610 | This should only be thrown from the constructor of a ShortcutHandler | ||
2441 | 611 | subclass, and only to indicate that the provided shortcut is invalid | ||
2442 | 612 | for that ShortcutHandler class. | ||
2443 | 613 | ''' | ||
2444 | 614 | pass | ||
2445 | 615 | |||
2446 | 616 | |||
2447 | 617 | # vi: ts=4 expandtab | ||
2448 | diff --git a/softwareproperties/shortcuts.py b/softwareproperties/shortcuts.py | |||
2449 | index c5f246c..f49321d 100644 | |||
2450 | --- a/softwareproperties/shortcuts.py | |||
2451 | +++ b/softwareproperties/shortcuts.py | |||
2452 | @@ -1,4 +1,4 @@ | |||
2454 | 1 | # Copyright (c) 2013 Canonical Ltd. | 1 | # Copyright (c) 2013-2019 Canonical Ltd. |
2455 | 2 | # | 2 | # |
2456 | 3 | # Author: Scott Moser <smoser@ubuntu.com> | 3 | # Author: Scott Moser <smoser@ubuntu.com> |
2457 | 4 | # | 4 | # |
2458 | @@ -17,41 +17,31 @@ | |||
2459 | 17 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 | 17 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 |
2460 | 18 | # USA | 18 | # USA |
2461 | 19 | 19 | ||
2462 | 20 | import aptsources.distro | ||
2463 | 21 | from gettext import gettext as _ | 20 | from gettext import gettext as _ |
2464 | 22 | 21 | ||
2466 | 23 | _DEF_CODENAME = aptsources.distro.get_distro().codename | 22 | from softwareproperties.cloudarchive import CloudArchiveShortcutHandler |
2467 | 23 | from softwareproperties.ppa import PPAShortcutHandler | ||
2468 | 24 | from softwareproperties.shortcuthandler import InvalidShortcutException | ||
2469 | 25 | from softwareproperties.sourceslist import SourcesListShortcutHandler | ||
2470 | 26 | from softwareproperties.uri import URIShortcutHandler | ||
2471 | 24 | 27 | ||
2472 | 25 | 28 | ||
2479 | 26 | class ShortcutHandler(object): | 29 | SHORTCUT_HANDLERS = [ |
2480 | 27 | # the defeault ShortcutHandler only handles actual apt lines. | 30 | PPAShortcutHandler, |
2481 | 28 | # ie, 'shortcut' here is a line like you'd find in /etc/apt/sources.list: | 31 | CloudArchiveShortcutHandler, |
2482 | 29 | # deb MIRROR RELEASE-POCKET COMPONENT | 32 | SourcesListShortcutHandler, |
2483 | 30 | def __init__(self, shortcut): | 33 | URIShortcutHandler, |
2484 | 31 | self.shortcut = shortcut | 34 | ] |
2485 | 32 | 35 | ||
2486 | 33 | def add_key(self, keyserver=None): | ||
2487 | 34 | return True | ||
2488 | 35 | 36 | ||
2491 | 36 | def expand(self, codename=None, distro=None): | 37 | def shortcut_handler(shortcut, **kwargs): |
2492 | 37 | return (self.shortcut, None) | 38 | for handler in SHORTCUT_HANDLERS: |
2493 | 39 | try: | ||
2494 | 40 | return handler(shortcut, **kwargs) | ||
2495 | 41 | except InvalidShortcutException: | ||
2496 | 42 | pass | ||
2497 | 38 | 43 | ||
2503 | 39 | def info(self): | 44 | raise InvalidShortcutException(_("Unable to handle input '%s'") % shortcut) |
2499 | 40 | return { | ||
2500 | 41 | 'description': _("No description available for '%(shortcut)s'") % | ||
2501 | 42 | {'shortcut': self.shortcut}, | ||
2502 | 43 | 'web_link': _("web link unavailable")} | ||
2504 | 44 | 45 | ||
2505 | 45 | def should_confirm(self): | ||
2506 | 46 | return False | ||
2507 | 47 | |||
2508 | 48 | |||
2509 | 49 | class ShortcutException(Exception): | ||
2510 | 50 | pass | ||
2511 | 51 | |||
2512 | 52 | |||
2513 | 53 | def shortcut_handler(shortcut): | ||
2514 | 54 | # this is the default shortcut handler, so it matches anything | ||
2515 | 55 | return ShortcutHandler(shortcut) | ||
2516 | 56 | 46 | ||
2517 | 57 | # vi: ts=4 expandtab | 47 | # vi: ts=4 expandtab |
2518 | diff --git a/softwareproperties/sourceslist.py b/softwareproperties/sourceslist.py | |||
2519 | 58 | new file mode 100644 | 48 | new file mode 100644 |
2520 | index 0000000..407dc94 | |||
2521 | --- /dev/null | |||
2522 | +++ b/softwareproperties/sourceslist.py | |||
2523 | @@ -0,0 +1,56 @@ | |||
2524 | 1 | # Copyright (c) 2019 Canonical Ltd. | ||
2525 | 2 | # | ||
2526 | 3 | # This program is free software; you can redistribute it and/or | ||
2527 | 4 | # modify it under the terms of the GNU General Public License as | ||
2528 | 5 | # published by the Free Software Foundation; either version 2 of the | ||
2529 | 6 | # License, or (at your option) any later version. | ||
2530 | 7 | # | ||
2531 | 8 | # This program is distributed in the hope that it will be useful, | ||
2532 | 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2533 | 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2534 | 11 | # GNU General Public License for more details. | ||
2535 | 12 | # | ||
2536 | 13 | # You should have received a copy of the GNU General Public License | ||
2537 | 14 | # along with this program; if not, write to the Free Software | ||
2538 | 15 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 | ||
2539 | 16 | # USA | ||
2540 | 17 | |||
2541 | 18 | from gettext import gettext as _ | ||
2542 | 19 | |||
2543 | 20 | from aptsources.sourceslist import SourceEntry | ||
2544 | 21 | |||
2545 | 22 | from softwareproperties.shortcuthandler import (ShortcutHandler, InvalidShortcutException) | ||
2546 | 23 | |||
2547 | 24 | from urllib.parse import urlparse | ||
2548 | 25 | |||
2549 | 26 | |||
2550 | 27 | SOURCESLIST_FILE_PREFIX = "archive_uri" | ||
2551 | 28 | |||
2552 | 29 | class SourcesListShortcutHandler(ShortcutHandler): | ||
2553 | 30 | def __init__(self, shortcut, ignore_line_comps=False, **kwargs): | ||
2554 | 31 | super(SourcesListShortcutHandler, self).__init__(shortcut, **kwargs) | ||
2555 | 32 | |||
2556 | 33 | entry = SourceEntry(shortcut) | ||
2557 | 34 | if entry.invalid: | ||
2558 | 35 | raise InvalidShortcutException(_("Invalid sources.list line: '%s'") % shortcut) | ||
2559 | 36 | |||
2560 | 37 | uri = entry.uri | ||
2561 | 38 | if not self.is_valid_uri(uri): | ||
2562 | 39 | raise InvalidShortcutException(_("Invalid URI: '%s'") % uri) | ||
2563 | 40 | |||
2564 | 41 | # ignore_line_comps is used by URIShortcutHandler when no comps are provided | ||
2565 | 42 | if not ignore_line_comps: | ||
2566 | 43 | self.components = list(set(self.components) | set(entry.comps)) | ||
2567 | 44 | |||
2568 | 45 | parsed = urlparse(uri) | ||
2569 | 46 | |||
2570 | 47 | self._username = parsed.username | ||
2571 | 48 | self._password = parsed.password | ||
2572 | 49 | |||
2573 | 50 | entry.uri = self.uri_strip_auth(entry.uri) | ||
2574 | 51 | # must set _filebase first; _set_source_entry uses it to set entry.file | ||
2575 | 52 | self._filebase = f"{SOURCESLIST_FILE_PREFIX}-{entry.uri}" | ||
2576 | 53 | self._set_source_entry(str(entry)) | ||
2577 | 54 | |||
2578 | 55 | |||
2579 | 56 | # vi: ts=4 expandtab | ||
2580 | diff --git a/softwareproperties/uri.py b/softwareproperties/uri.py | |||
2581 | 0 | new file mode 100644 | 57 | new file mode 100644 |
2582 | index 0000000..a553a97 | |||
2583 | --- /dev/null | |||
2584 | +++ b/softwareproperties/uri.py | |||
2585 | @@ -0,0 +1,36 @@ | |||
2586 | 1 | # Copyright (c) 2019 Canonical Ltd. | ||
2587 | 2 | # | ||
2588 | 3 | # This program is free software; you can redistribute it and/or | ||
2589 | 4 | # modify it under the terms of the GNU General Public License as | ||
2590 | 5 | # published by the Free Software Foundation; either version 2 of the | ||
2591 | 6 | # License, or (at your option) any later version. | ||
2592 | 7 | # | ||
2593 | 8 | # This program is distributed in the hope that it will be useful, | ||
2594 | 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2595 | 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2596 | 11 | # GNU General Public License for more details. | ||
2597 | 12 | # | ||
2598 | 13 | # You should have received a copy of the GNU General Public License | ||
2599 | 14 | # along with this program; if not, write to the Free Software | ||
2600 | 15 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 | ||
2601 | 16 | # USA | ||
2602 | 17 | |||
2603 | 18 | from aptsources.distro import get_distro | ||
2604 | 19 | |||
2605 | 20 | from softwareproperties.sourceslist import SourcesListShortcutHandler | ||
2606 | 21 | |||
2607 | 22 | |||
2608 | 23 | class URIShortcutHandler(SourcesListShortcutHandler): | ||
2609 | 24 | def __init__(self, shortcut, **kwargs): | ||
2610 | 25 | (uri, _, comps) = shortcut.strip().partition(' ') | ||
2611 | 26 | |||
2612 | 27 | # can't use self.codename here, as we haven't called superclass constructor yet | ||
2613 | 28 | distro = get_distro() | ||
2614 | 29 | codename = kwargs.get('codename', distro.codename) | ||
2615 | 30 | |||
2616 | 31 | line = ('%s %s %s %s' % (distro.binary_type, uri, codename, comps or 'main')) | ||
2617 | 32 | |||
2618 | 33 | super(URIShortcutHandler, self).__init__(line, ignore_line_comps=not comps, **kwargs) | ||
2619 | 34 | |||
2620 | 35 | |||
2621 | 36 | # vi: ts=4 expandtab | ||
2622 | diff --git a/tests/aptroot/etc/apt/apt.conf.d/.keep b/tests/aptroot/etc/apt/apt.conf.d/.keep | |||
2623 | 0 | deleted file mode 100644 | 37 | deleted file mode 100644 |
2624 | index e69de29..0000000 | |||
2625 | --- a/tests/aptroot/etc/apt/apt.conf.d/.keep | |||
2626 | +++ /dev/null | |||
2627 | diff --git a/tests/test_aptsources.py b/tests/test_aptsources.py | |||
2628 | index 83f28a1..13043ff 100755 | |||
2629 | --- a/tests/test_aptsources.py | |||
2630 | +++ b/tests/test_aptsources.py | |||
2631 | @@ -1,4 +1,4 @@ | |||
2633 | 1 | #!/usr/bin/python | 1 | #!/usr/bin/python3 |
2634 | 2 | 2 | ||
2635 | 3 | from __future__ import print_function | 3 | from __future__ import print_function |
2636 | 4 | 4 | ||
2637 | diff --git a/tests/test_dbus.py b/tests/test_dbus.py | |||
2638 | index 2505c1e..2099a72 100755 | |||
2639 | --- a/tests/test_dbus.py | |||
2640 | +++ b/tests/test_dbus.py | |||
2641 | @@ -1,4 +1,4 @@ | |||
2643 | 1 | #!/usr/bin/python | 1 | #!/usr/bin/python3 |
2644 | 2 | # -*- coding: utf-8 -*- | 2 | # -*- coding: utf-8 -*- |
2645 | 3 | 3 | ||
2646 | 4 | from __future__ import print_function | 4 | from __future__ import print_function |
2647 | @@ -7,6 +7,7 @@ from gi.repository import GLib, Gio | |||
2648 | 7 | 7 | ||
2649 | 8 | import apt_pkg | 8 | import apt_pkg |
2650 | 9 | import aptsources.distro | 9 | import aptsources.distro |
2651 | 10 | import aptsources.sourceslist | ||
2652 | 10 | 11 | ||
2653 | 11 | import dbus | 12 | import dbus |
2654 | 12 | import logging | 13 | import logging |
2655 | @@ -62,9 +63,8 @@ def clear_apt_config(): | |||
2656 | 62 | if os.path.isfile(path): | 63 | if os.path.isfile(path): |
2657 | 63 | os.unlink(path) | 64 | os.unlink(path) |
2658 | 64 | 65 | ||
2662 | 65 | if not os.path.exists(os.path.join(etc_apt, "apt.conf.d")): | 66 | for d in ["apt.conf.d", "sources.list.d", "trusted.gpg.d", "auth.conf.d"]: |
2663 | 66 | os.mkdir(os.path.join(etc_apt, "apt.conf.d")) | 67 | os.makedirs(os.path.join(etc_apt, d), exist_ok=True) |
2661 | 67 | |||
2664 | 68 | 68 | ||
2665 | 69 | def create_sources_list(): | 69 | def create_sources_list(): |
2666 | 70 | s = get_test_source_line() + "\n" | 70 | s = get_test_source_line() + "\n" |
2667 | @@ -147,7 +147,7 @@ class TestDBus(unittest.TestCase): | |||
2668 | 147 | # keep track of signal emissions | 147 | # keep track of signal emissions |
2669 | 148 | self.sources_list_count = 0 | 148 | self.sources_list_count = 0 |
2670 | 149 | self.distro_release = get_distro_release() | 149 | self.distro_release = get_distro_release() |
2672 | 150 | self.sources_list_path = create_sources_list() | 150 | create_sources_list() |
2673 | 151 | # create the client proxy | 151 | # create the client proxy |
2674 | 152 | bus = dbus.SessionBus(private=True, mainloop=DBusGMainLoop()) | 152 | bus = dbus.SessionBus(private=True, mainloop=DBusGMainLoop()) |
2675 | 153 | proxy = bus.get_object("com.ubuntu.SoftwareProperties", "/") | 153 | proxy = bus.get_object("com.ubuntu.SoftwareProperties", "/") |
2676 | @@ -164,10 +164,17 @@ class TestDBus(unittest.TestCase): | |||
2677 | 164 | #print("_on_modified_sources_list") | 164 | #print("_on_modified_sources_list") |
2678 | 165 | self.sources_list_count += 1 | 165 | self.sources_list_count += 1 |
2679 | 166 | 166 | ||
2680 | 167 | @property | ||
2681 | 168 | def sourceslist(self): | ||
2682 | 169 | return ''.join([str(e) for e in aptsources.sourceslist.SourcesList()]) | ||
2683 | 170 | |||
2684 | 171 | @property | ||
2685 | 172 | def enabled_sourceslist(self): | ||
2686 | 173 | return ''.join([str(e) for e in aptsources.sourceslist.SourcesList() | ||
2687 | 174 | if not e.invalid and not e.disabled]) | ||
2688 | 175 | |||
2689 | 167 | def _debug_sourceslist(self, text=""): | 176 | def _debug_sourceslist(self, text=""): |
2693 | 168 | with open(self.sources_list_path) as f: | 177 | logging.debug("sourceslist: %s '%s'" % (text, self.sourceslist)) |
2691 | 169 | sourceslist = f.read() | ||
2692 | 170 | logging.debug("sourceslist: %s '%s'" % (text, sourceslist)) | ||
2694 | 171 | 178 | ||
2695 | 172 | # this is an async call - give it a few seconds to catch up with what we expect | 179 | # this is an async call - give it a few seconds to catch up with what we expect |
2696 | 173 | def _assert_eventually(self, prop, n): | 180 | def _assert_eventually(self, prop, n): |
2697 | @@ -181,62 +188,44 @@ class TestDBus(unittest.TestCase): | |||
2698 | 181 | 188 | ||
2699 | 182 | def test_enable_disable_component(self): | 189 | def test_enable_disable_component(self): |
2700 | 183 | # ensure its not there | 190 | # ensure its not there |
2704 | 184 | with open(self.sources_list_path) as f: | 191 | self.assertNotIn("universe", self.sourceslist) |
2702 | 185 | sourceslist = f.read() | ||
2703 | 186 | self.assertFalse("universe" in sourceslist) | ||
2705 | 187 | # enable | 192 | # enable |
2706 | 188 | self.iface.EnableComponent("universe") | 193 | self.iface.EnableComponent("universe") |
2707 | 189 | self._debug_sourceslist("2") | 194 | self._debug_sourceslist("2") |
2711 | 190 | with open(self.sources_list_path) as f: | 195 | self.assertIn("universe", self.sourceslist) |
2709 | 191 | sourceslist = f.read() | ||
2710 | 192 | self.assertTrue("universe" in sourceslist) | ||
2712 | 193 | # disable again | 196 | # disable again |
2713 | 194 | self.iface.DisableComponent("universe") | 197 | self.iface.DisableComponent("universe") |
2714 | 195 | self._debug_sourceslist("3") | 198 | self._debug_sourceslist("3") |
2718 | 196 | with open(self.sources_list_path) as f: | 199 | self.assertNotIn("universe", self.sourceslist) |
2716 | 197 | sourceslist = f.read() | ||
2717 | 198 | self.assertFalse("universe" in sourceslist) | ||
2719 | 199 | self._assert_eventually("sources_list_count", 2) | 200 | self._assert_eventually("sources_list_count", 2) |
2720 | 200 | 201 | ||
2721 | 201 | def test_enable_enable_disable_source_code_sources(self): | 202 | def test_enable_enable_disable_source_code_sources(self): |
2722 | 202 | # ensure its not there | 203 | # ensure its not there |
2723 | 203 | self._debug_sourceslist("4") | 204 | self._debug_sourceslist("4") |
2727 | 204 | with open(self.sources_list_path) as f: | 205 | self.assertNotIn('deb-src', self.enabled_sourceslist) |
2725 | 205 | sourceslist = f.read() | ||
2726 | 206 | self.assertFalse("deb-src" in sourceslist) | ||
2728 | 207 | # enable | 206 | # enable |
2729 | 208 | self.iface.EnableSourceCodeSources() | 207 | self.iface.EnableSourceCodeSources() |
2730 | 209 | self._debug_sourceslist("5") | 208 | self._debug_sourceslist("5") |
2734 | 210 | with open(self.sources_list_path) as f: | 209 | self.assertIn('deb-src', self.enabled_sourceslist) |
2732 | 211 | sourceslist = f.read() | ||
2733 | 212 | self.assertTrue("deb-src" in sourceslist) | ||
2735 | 213 | # disable again | 210 | # disable again |
2736 | 214 | self.iface.DisableSourceCodeSources() | 211 | self.iface.DisableSourceCodeSources() |
2737 | 215 | self._debug_sourceslist("6") | 212 | self._debug_sourceslist("6") |
2741 | 216 | with open(self.sources_list_path) as f: | 213 | self.assertNotIn('deb-src', self.enabled_sourceslist) |
2739 | 217 | sourceslist = f.read() | ||
2740 | 218 | self.assertFalse("deb-src" in sourceslist) | ||
2742 | 219 | self._assert_eventually("sources_list_count", 3) | 214 | self._assert_eventually("sources_list_count", 3) |
2743 | 220 | 215 | ||
2744 | 221 | def test_enable_child_source(self): | 216 | def test_enable_child_source(self): |
2745 | 222 | child_source = "%s-updates" % self.distro_release | 217 | child_source = "%s-updates" % self.distro_release |
2746 | 223 | # ensure its not there | 218 | # ensure its not there |
2747 | 224 | self._debug_sourceslist("7") | 219 | self._debug_sourceslist("7") |
2751 | 225 | with open(self.sources_list_path) as f: | 220 | self.assertNotIn(child_source, self.sourceslist) |
2749 | 226 | sourceslist = f.read() | ||
2750 | 227 | self.assertFalse(child_source in sourceslist) | ||
2752 | 228 | # enable | 221 | # enable |
2753 | 229 | self.iface.EnableChildSource(child_source) | 222 | self.iface.EnableChildSource(child_source) |
2754 | 230 | self._debug_sourceslist("8") | 223 | self._debug_sourceslist("8") |
2758 | 231 | with open(self.sources_list_path) as f: | 224 | self.assertIn(child_source, self.sourceslist) |
2756 | 232 | sourceslist = f.read() | ||
2757 | 233 | self.assertTrue(child_source in sourceslist) | ||
2759 | 234 | # disable again | 225 | # disable again |
2760 | 235 | self.iface.DisableChildSource(child_source) | 226 | self.iface.DisableChildSource(child_source) |
2761 | 236 | self._debug_sourceslist("9") | 227 | self._debug_sourceslist("9") |
2765 | 237 | with open(self.sources_list_path) as f: | 228 | self.assertNotIn(child_source, self.sourceslist) |
2763 | 238 | sourceslist = f.read() | ||
2764 | 239 | self.assertFalse(child_source in sourceslist) | ||
2766 | 240 | self._assert_eventually("sources_list_count", 2) | 229 | self._assert_eventually("sources_list_count", 2) |
2767 | 241 | 230 | ||
2768 | 242 | def test_toggle_source(self): | 231 | def test_toggle_source(self): |
2769 | @@ -244,17 +233,13 @@ class TestDBus(unittest.TestCase): | |||
2770 | 244 | source = get_test_source_line() | 233 | source = get_test_source_line() |
2771 | 245 | self.iface.ToggleSourceUse(source) | 234 | self.iface.ToggleSourceUse(source) |
2772 | 246 | self._debug_sourceslist("10") | 235 | self._debug_sourceslist("10") |
2773 | 247 | with open(self.sources_list_path) as f: | ||
2774 | 248 | sourceslist = f.read() | ||
2775 | 249 | primary_debline = "# deb %s" % PRIMARY_MIRROR | 236 | primary_debline = "# deb %s" % PRIMARY_MIRROR |
2777 | 250 | self.assertTrue(primary_debline in sourceslist) | 237 | self.assertIn(primary_debline, self.sourceslist) |
2778 | 251 | # to disable the line again, we need to match the new "#" | 238 | # to disable the line again, we need to match the new "#" |
2779 | 252 | source = "# " + source | 239 | source = "# " + source |
2780 | 253 | self.iface.ToggleSourceUse(source) | 240 | self.iface.ToggleSourceUse(source) |
2781 | 254 | self._debug_sourceslist("11") | 241 | self._debug_sourceslist("11") |
2785 | 255 | with open(self.sources_list_path) as f: | 242 | self.assertNotIn(primary_debline, self.sourceslist) |
2783 | 256 | sourceslist = f.read() | ||
2784 | 257 | self.assertFalse(primary_debline in sourceslist) | ||
2786 | 258 | 243 | ||
2787 | 259 | self._assert_eventually("sources_list_count", 2) | 244 | self._assert_eventually("sources_list_count", 2) |
2788 | 260 | 245 | ||
2789 | @@ -264,10 +249,8 @@ class TestDBus(unittest.TestCase): | |||
2790 | 264 | source_new = "deb http://xxx/ %s" % self.distro_release | 249 | source_new = "deb http://xxx/ %s" % self.distro_release |
2791 | 265 | self.iface.ReplaceSourceEntry(source, source_new) | 250 | self.iface.ReplaceSourceEntry(source, source_new) |
2792 | 266 | self._debug_sourceslist("11") | 251 | self._debug_sourceslist("11") |
2797 | 267 | with open(self.sources_list_path) as f: | 252 | self.assertIn(source_new, self.sourceslist) |
2798 | 268 | sourceslist = f.read() | 253 | self.assertNotIn(source, self.sourceslist) |
2795 | 269 | self.assertTrue(source_new in sourceslist) | ||
2796 | 270 | self.assertFalse(source in sourceslist) | ||
2799 | 271 | self._assert_eventually("sources_list_count", 1) | 254 | self._assert_eventually("sources_list_count", 1) |
2800 | 272 | self.iface.ReplaceSourceEntry(source_new, source) | 255 | self.iface.ReplaceSourceEntry(source_new, source) |
2801 | 273 | self._assert_eventually("sources_list_count", 2) | 256 | self._assert_eventually("sources_list_count", 2) |
2802 | @@ -278,19 +261,19 @@ class TestDBus(unittest.TestCase): | |||
2803 | 278 | "aptroot", "etc", "popularity-contest.conf") | 261 | "aptroot", "etc", "popularity-contest.conf") |
2804 | 279 | with open(popcon_p) as f: | 262 | with open(popcon_p) as f: |
2805 | 280 | popcon = f.read() | 263 | popcon = f.read() |
2807 | 281 | self.assertTrue('PARTICIPATE="no"' in popcon) | 264 | self.assertIn('PARTICIPATE="no"', popcon) |
2808 | 282 | # toggle | 265 | # toggle |
2809 | 283 | self.iface.SetPopconPariticipation(True) | 266 | self.iface.SetPopconPariticipation(True) |
2810 | 284 | with open(popcon_p) as f: | 267 | with open(popcon_p) as f: |
2811 | 285 | popcon = f.read() | 268 | popcon = f.read() |
2814 | 286 | self.assertTrue('PARTICIPATE="yes"' in popcon) | 269 | self.assertIn('PARTICIPATE="yes"', popcon) |
2815 | 287 | self.assertFalse('PARTICIPATE="no"' in popcon) | 270 | self.assertNotIn('PARTICIPATE="no"', popcon) |
2816 | 288 | # and back | 271 | # and back |
2817 | 289 | self.iface.SetPopconPariticipation(False) | 272 | self.iface.SetPopconPariticipation(False) |
2818 | 290 | with open(popcon_p) as f: | 273 | with open(popcon_p) as f: |
2819 | 291 | popcon = f.read() | 274 | popcon = f.read() |
2822 | 292 | self.assertFalse('PARTICIPATE="yes"' in popcon) | 275 | self.assertNotIn('PARTICIPATE="yes"', popcon) |
2823 | 293 | self.assertTrue('PARTICIPATE="no"' in popcon) | 276 | self.assertIn('PARTICIPATE="no"', popcon) |
2824 | 294 | 277 | ||
2825 | 295 | def test_updates_automation(self): | 278 | def test_updates_automation(self): |
2826 | 296 | states = [UPDATE_INST_SEC, UPDATE_DOWNLOAD, UPDATE_NOTIFY] | 279 | states = [UPDATE_INST_SEC, UPDATE_DOWNLOAD, UPDATE_NOTIFY] |
2827 | @@ -301,19 +284,19 @@ class TestDBus(unittest.TestCase): | |||
2828 | 301 | "10periodic") | 284 | "10periodic") |
2829 | 302 | with open(cfg) as f: | 285 | with open(cfg) as f: |
2830 | 303 | config = f.read() | 286 | config = f.read() |
2832 | 304 | self.assertTrue('APT::Periodic::Unattended-Upgrade "1";' in config) | 287 | self.assertIn('APT::Periodic::Unattended-Upgrade "1";', config) |
2833 | 305 | # download | 288 | # download |
2834 | 306 | self.iface.SetUpdateAutomationLevel(states[1]) | 289 | self.iface.SetUpdateAutomationLevel(states[1]) |
2835 | 307 | with open(cfg) as f: | 290 | with open(cfg) as f: |
2836 | 308 | config = f.read() | 291 | config = f.read() |
2839 | 309 | self.assertTrue('APT::Periodic::Unattended-Upgrade "0";' in config) | 292 | self.assertIn('APT::Periodic::Unattended-Upgrade "0";', config) |
2840 | 310 | self.assertTrue('APT::Periodic::Download-Upgradeable-Packages "1";' in config) | 293 | self.assertIn('APT::Periodic::Download-Upgradeable-Packages "1";', config) |
2841 | 311 | # notify | 294 | # notify |
2842 | 312 | self.iface.SetUpdateAutomationLevel(states[2]) | 295 | self.iface.SetUpdateAutomationLevel(states[2]) |
2843 | 313 | with open(cfg) as f: | 296 | with open(cfg) as f: |
2844 | 314 | config = f.read() | 297 | config = f.read() |
2847 | 315 | self.assertTrue('APT::Periodic::Unattended-Upgrade "0";' in config) | 298 | self.assertIn('APT::Periodic::Unattended-Upgrade "0";', config) |
2848 | 316 | self.assertTrue('APT::Periodic::Download-Upgradeable-Packages "0";' in config) | 299 | self.assertIn('APT::Periodic::Download-Upgradeable-Packages "0";', config) |
2849 | 317 | 300 | ||
2850 | 318 | def test_updates_interval(self): | 301 | def test_updates_interval(self): |
2851 | 319 | # interval | 302 | # interval |
2852 | @@ -329,30 +312,26 @@ class TestDBus(unittest.TestCase): | |||
2853 | 329 | self.iface.SetUpdateInterval(1) | 312 | self.iface.SetUpdateInterval(1) |
2854 | 330 | with open(cfg) as f: | 313 | with open(cfg) as f: |
2855 | 331 | config = f.read() | 314 | config = f.read() |
2857 | 332 | self.assertTrue('APT::Periodic::Update-Package-Lists "1";' in config) | 315 | self.assertIn('APT::Periodic::Update-Package-Lists "1";', config) |
2858 | 333 | self.iface.SetUpdateInterval(0) | 316 | self.iface.SetUpdateInterval(0) |
2859 | 334 | with open(cfg) as f: | 317 | with open(cfg) as f: |
2860 | 335 | config = f.read() | 318 | config = f.read() |
2862 | 336 | self.assertTrue('APT::Periodic::Update-Package-Lists "0";' in config) | 319 | self.assertIn('APT::Periodic::Update-Package-Lists "0";', config) |
2863 | 337 | 320 | ||
2864 | 338 | def test_add_remove_source_by_line(self): | 321 | def test_add_remove_source_by_line(self): |
2865 | 339 | # add invalid | 322 | # add invalid |
2866 | 340 | res = self.iface.AddSourceFromLine("xxx") | 323 | res = self.iface.AddSourceFromLine("xxx") |
2867 | 341 | self.assertFalse(res) | 324 | self.assertFalse(res) |
2868 | 342 | # add real | 325 | # add real |
2870 | 343 | s = "deb http//ppa.launchpad.net/ foo bar" | 326 | s = "deb http://ppa.launchpad.net/ foo bar" |
2871 | 344 | self.iface.AddSourceFromLine(s) | 327 | self.iface.AddSourceFromLine(s) |
2876 | 345 | with open(self.sources_list_path) as f: | 328 | self.assertIn(s, self.sourceslist) |
2877 | 346 | sourceslist = f.read() | 329 | self.assertIn(s.replace("deb", "# deb-src"), self.sourceslist) |
2874 | 347 | self.assertTrue(s in sourceslist) | ||
2875 | 348 | self.assertTrue(s.replace("deb", "# deb-src") in sourceslist) | ||
2878 | 349 | # remove again | 330 | # remove again |
2879 | 350 | self.iface.RemoveSource(s) | 331 | self.iface.RemoveSource(s) |
2880 | 351 | self.iface.RemoveSource(s.replace("deb", "deb-src")) | 332 | self.iface.RemoveSource(s.replace("deb", "deb-src")) |
2885 | 352 | with open(self.sources_list_path) as f: | 333 | self.assertNotIn(s, self.sourceslist) |
2886 | 353 | sourceslist = f.read() | 334 | self.assertNotIn(s.replace("deb", "# deb-src"), self.sourceslist) |
2883 | 354 | self.assertFalse(s in sourceslist) | ||
2884 | 355 | self.assertFalse(s.replace("deb", "# deb-src") in sourceslist) | ||
2887 | 356 | self._assert_eventually("sources_list_count", 4) | 335 | self._assert_eventually("sources_list_count", 4) |
2888 | 357 | 336 | ||
2889 | 358 | def test_add_gpg_key(self): | 337 | def test_add_gpg_key(self): |
2890 | diff --git a/tests/test_lp.py b/tests/test_lp.py | |||
2891 | 359 | deleted file mode 100755 | 338 | deleted file mode 100755 |
2892 | index a732ab0..0000000 | |||
2893 | --- a/tests/test_lp.py | |||
2894 | +++ /dev/null | |||
2895 | @@ -1,167 +0,0 @@ | |||
2896 | 1 | #!/usr/bin/python | ||
2897 | 2 | |||
2898 | 3 | import apt_pkg | ||
2899 | 4 | |||
2900 | 5 | import os | ||
2901 | 6 | import unittest | ||
2902 | 7 | import sys | ||
2903 | 8 | sys.path.insert(0, "..") | ||
2904 | 9 | |||
2905 | 10 | from mock import patch | ||
2906 | 11 | |||
2907 | 12 | import softwareproperties.ppa | ||
2908 | 13 | from softwareproperties.ppa import ( | ||
2909 | 14 | AddPPASigningKeyThread, | ||
2910 | 15 | mangle_ppa_shortcut, | ||
2911 | 16 | verify_keyid_is_v4, | ||
2912 | 17 | ) | ||
2913 | 18 | |||
2914 | 19 | |||
2915 | 20 | MOCK_PPA_INFO={ | ||
2916 | 21 | "displayname": "PPA for Michael Vogt", | ||
2917 | 22 | "web_link": "https://launchpad.net/~mvo/+archive/ppa", | ||
2918 | 23 | "signing_key_fingerprint": "019A25FED88F961763935D7F129196470EB12F05", | ||
2919 | 24 | "name": "ppa", | ||
2920 | 25 | 'distribution_link': 'https://launchpad.net/api/1.0/ubuntu', | ||
2921 | 26 | 'owner_link': 'https://launchpad.net/api/1.0/~mvo', | ||
2922 | 27 | 'reference': '~mvo/ubuntu/ppa', | ||
2923 | 28 | 'self_link': 'https://launchpad.net/api/devel/~mvo/+archive/ubuntu/ppa', | ||
2924 | 29 | } | ||
2925 | 30 | |||
2926 | 31 | MOCK_KEY=""" | ||
2927 | 32 | -----BEGIN PGP PUBLIC KEY BLOCK----- | ||
2928 | 33 | Version: SKS 1.1.6 | ||
2929 | 34 | Comment: Hostname: keyserver.ubuntu.com | ||
2930 | 35 | |||
2931 | 36 | mI0ESXP67wEEAN2m3xWkAP0p1erHbJx1wYBCL6tLqWXESx1BmF0htLzdD9lfsUYiNs+Zgg3w | ||
2932 | 37 | uU0PrQIcqZtyTESh514tw3KQ+OAK2I0a2XJR99lXPksiKoxaOOsr0pTVWDYuIlfV3yfmXvnK | ||
2933 | 38 | FZSmaMjjKuqQbCwZe8Ev7yry9Gh9pM5Y87MbNT05ABEBAAG0HkxhdW5jaHBhZCBQUEEgZm9y | ||
2934 | 39 | IE1pY2hhZWwgVm9ndIi2BBMBAgAgBQJJc/rvAhsDBgsJCAcDAgQVAggDBBYCAwECHgECF4AA | ||
2935 | 40 | CgkQEpGWRw6xLwVofAP/YyU3YykXbr8p7wRp1EpFlDmtbPlFXp00gt4Cqlu2AWVOkwkVoMRQ | ||
2936 | 41 | Ncb7wog2Z6u7KyUhD8pgC2FEL0+FQjyNemv7D0OYBG+6DLdjtRsv0CumLdWFmviU96j3OcwT | ||
2937 | 42 | G2GkIC/eB2maTrV/vj7vlZ0Qe/T1NL6XLpr0A6Rg6JAtkFM= | ||
2938 | 43 | =SMbJ | ||
2939 | 44 | -----END PGP PUBLIC KEY BLOCK----- | ||
2940 | 45 | """ | ||
2941 | 46 | |||
2942 | 47 | MOCK_SECOND_KEY=""" | ||
2943 | 48 | -----BEGIN PGP PUBLIC KEY BLOCK----- | ||
2944 | 49 | Version: SKS 1.1.6 | ||
2945 | 50 | Comment: Hostname: keyserver.ubuntu.com | ||
2946 | 51 | |||
2947 | 52 | mI0ESX34EgEEAOTzplZO3TXmb9dRLu7kOuIEia21e4gwQ/RQe+LD7HdhikcETjf2Ruu0mn6S | ||
2948 | 53 | sgPLL+duhKxmv6ZciLUgkk0qEDCZuR6BPxdgAIwqmQmFipcv6UTMQitRPUa9WlPU37Qg+joL | ||
2949 | 54 | cTBUdamnVq+yJhLmnuO44UWAty85nNJzDd29gxqXABEBAAG0LUxhdW5jaHBhZCBQUEEgZm9y | ||
2950 | 55 | INCU0LzQuNGC0YDQuNC5INCb0LXQtNC60L7Qsoi2BBMBAgAgBQJJffgSAhsDBgsJCAcDAgQV | ||
2951 | 56 | AggDBBYCAwECHgECF4AACgkQFXlR/kAx0oeuSwQAuhhgWgeeG3F9XMYDqgJShzMSeQOLMKBq | ||
2952 | 57 | 6mNFEL1sDhRdbinf7rwuQFXDSSNCj8/PLa3DF/u09tAm6CTi10iwxxbXf16pTq21gxCA3/xS | ||
2953 | 58 | fszv352yZpcN85MD5aozqv7qUCGOQ9Gey7JzgD7L4wMEjyRScVjx1chfLgyapdj822E= | ||
2954 | 59 | =pdql | ||
2955 | 60 | -----END PGP PUBLIC KEY BLOCK----- | ||
2956 | 61 | """ | ||
2957 | 62 | |||
2958 | 63 | class LaunchpadPPATestCase(unittest.TestCase): | ||
2959 | 64 | |||
2960 | 65 | @classmethod | ||
2961 | 66 | def setUpClass(cls): | ||
2962 | 67 | for k in apt_pkg.config.keys(): | ||
2963 | 68 | apt_pkg.config.clear(k) | ||
2964 | 69 | apt_pkg.init() | ||
2965 | 70 | |||
2966 | 71 | @unittest.skipUnless( | ||
2967 | 72 | "TEST_ONLINE" in os.environ, | ||
2968 | 73 | "skipping online tests unless TEST_ONLINE environment variable is set") | ||
2969 | 74 | @unittest.skipUnless( | ||
2970 | 75 | sys.version_info[0] > 2, | ||
2971 | 76 | "pycurl doesn't raise SSL exceptions anymore it seems") | ||
2972 | 77 | def test_ppa_info_from_lp(self): | ||
2973 | 78 | # use correct data | ||
2974 | 79 | info = softwareproperties.ppa.get_ppa_info_from_lp("mvo", "ppa") | ||
2975 | 80 | self.assertNotEqual(info, {}) | ||
2976 | 81 | self.assertEqual(info["name"], "ppa") | ||
2977 | 82 | # use empty CERT file | ||
2978 | 83 | softwareproperties.ppa.LAUNCHPAD_PPA_CERT = "/dev/null" | ||
2979 | 84 | with self.assertRaises(Exception): | ||
2980 | 85 | softwareproperties.ppa.get_ppa_info_from_lp("mvo", "ppa") | ||
2981 | 86 | |||
2982 | 87 | def test_mangle_ppa_shortcut(self): | ||
2983 | 88 | self.assertEqual("~mvo/ubuntu/ppa", mangle_ppa_shortcut("ppa:mvo")) | ||
2984 | 89 | self.assertEqual( | ||
2985 | 90 | "~mvo/ubuntu/compiz", mangle_ppa_shortcut("ppa:mvo/compiz")) | ||
2986 | 91 | self.assertEqual( | ||
2987 | 92 | "~mvo/ubuntu-rtm/compiz", | ||
2988 | 93 | mangle_ppa_shortcut("ppa:mvo/ubuntu-rtm/compiz")) | ||
2989 | 94 | |||
2990 | 95 | def test_mangle_ppa_shortcut_leading_slash(self): | ||
2991 | 96 | # Test for LP: #1426933 | ||
2992 | 97 | self.assertEqual("~gottcode/ubuntu/gcppa", | ||
2993 | 98 | mangle_ppa_shortcut("ppa:/gottcode/gcppa")) | ||
2994 | 99 | |||
2995 | 100 | def test_mangle_ppa_supports_no_ppa_colon_prefix(self): | ||
2996 | 101 | """mangle_ppa should also support input without 'ppa:'.""" | ||
2997 | 102 | self.assertEqual("~mvo/ubuntu/ppa", mangle_ppa_shortcut("~mvo/ppa")) | ||
2998 | 103 | |||
2999 | 104 | |||
3000 | 105 | class AddPPASigningKeyTestCase(unittest.TestCase): | ||
3001 | 106 | |||
3002 | 107 | @classmethod | ||
3003 | 108 | def setUpClass(cls): | ||
3004 | 109 | for k in apt_pkg.config.keys(): | ||
3005 | 110 | apt_pkg.config.clear(k) | ||
3006 | 111 | apt_pkg.init() | ||
3007 | 112 | cls.trustedgpg = os.path.join( | ||
3008 | 113 | os.path.dirname(__file__), "aptroot", "etc", "apt", "trusted.gpg.d") | ||
3009 | 114 | try: | ||
3010 | 115 | os.makedirs(cls.trustedgpg) | ||
3011 | 116 | except: | ||
3012 | 117 | pass | ||
3013 | 118 | |||
3014 | 119 | def setUp(self): | ||
3015 | 120 | self.t = AddPPASigningKeyThread("~mvo/ubuntu/ppa") | ||
3016 | 121 | |||
3017 | 122 | @patch("softwareproperties.ppa.get_ppa_info_from_lp") | ||
3018 | 123 | @patch("softwareproperties.ppa.subprocess") | ||
3019 | 124 | def test_fingerprint_len_check(self, mock_subprocess, mock_get_ppa_info): | ||
3020 | 125 | """Test that short keyids (<160bit) are rejected""" | ||
3021 | 126 | mock_ppa_info = MOCK_PPA_INFO.copy() | ||
3022 | 127 | mock_ppa_info["signing_key_fingerprint"] = "0EB12F05" | ||
3023 | 128 | mock_get_ppa_info.return_value = mock_ppa_info | ||
3024 | 129 | # do it | ||
3025 | 130 | res = self.t.add_ppa_signing_key("~mvo/ubuntu/ppa") | ||
3026 | 131 | self.assertFalse(res) | ||
3027 | 132 | self.assertFalse(mock_subprocess.Popen.called) | ||
3028 | 133 | self.assertFalse(mock_subprocess.call.called) | ||
3029 | 134 | |||
3030 | 135 | @patch("softwareproperties.ppa.get_ppa_info_from_lp") | ||
3031 | 136 | @patch("softwareproperties.ppa.get_info_from_https") | ||
3032 | 137 | def test_add_ppa_signing_key_wrong_fingerprint(self, mock_https, mock_get_ppa_info): | ||
3033 | 138 | mock_get_ppa_info.return_value = MOCK_PPA_INFO | ||
3034 | 139 | mock_https.return_value = MOCK_SECOND_KEY | ||
3035 | 140 | res = self.t.add_ppa_signing_key("~mvo/ubuntu/ppa") | ||
3036 | 141 | self.assertFalse(res) | ||
3037 | 142 | |||
3038 | 143 | @patch("softwareproperties.ppa.get_ppa_info_from_lp") | ||
3039 | 144 | @patch("softwareproperties.ppa.get_info_from_https") | ||
3040 | 145 | def test_add_ppa_signing_key_multiple_fingerprints(self, mock_https, mock_get_ppa_info): | ||
3041 | 146 | mock_get_ppa_info.return_value = MOCK_PPA_INFO | ||
3042 | 147 | mock_https.return_value = '\n'.join([MOCK_KEY, MOCK_SECOND_KEY]) | ||
3043 | 148 | res = self.t.add_ppa_signing_key("~mvo/ubuntu/ppa") | ||
3044 | 149 | self.assertFalse(res) | ||
3045 | 150 | |||
3046 | 151 | @patch("softwareproperties.ppa.get_ppa_info_from_lp") | ||
3047 | 152 | @patch("softwareproperties.ppa.get_info_from_https") | ||
3048 | 153 | @patch("apt_pkg.config") | ||
3049 | 154 | def test_add_ppa_signing_key_ok(self, mock_config, mock_https, mock_get_ppa_info): | ||
3050 | 155 | mock_get_ppa_info.return_value = MOCK_PPA_INFO | ||
3051 | 156 | mock_https.return_value = MOCK_KEY | ||
3052 | 157 | mock_config.find_dir.return_value = self.trustedgpg | ||
3053 | 158 | res = self.t.add_ppa_signing_key("~mvo/ubuntu/ppa") | ||
3054 | 159 | self.assertTrue(res) | ||
3055 | 160 | |||
3056 | 161 | def test_verify_keyid_is_v4(self): | ||
3057 | 162 | keyid = "0EB12F05" | ||
3058 | 163 | self.assertFalse(verify_keyid_is_v4(keyid)) | ||
3059 | 164 | |||
3060 | 165 | |||
3061 | 166 | if __name__ == "__main__": | ||
3062 | 167 | unittest.main() | ||
3063 | diff --git a/tests/test_pyflakes.py b/tests/test_pyflakes.py | |||
3064 | index b066e2d..83aef4a 100755 | |||
3065 | --- a/tests/test_pyflakes.py | |||
3066 | +++ b/tests/test_pyflakes.py | |||
3067 | @@ -3,7 +3,6 @@ import os | |||
3068 | 3 | import subprocess | 3 | import subprocess |
3069 | 4 | import unittest | 4 | import unittest |
3070 | 5 | 5 | ||
3071 | 6 | @unittest.skip("It's not clean") | ||
3072 | 7 | class TestPyflakesClean(unittest.TestCase): | 6 | class TestPyflakesClean(unittest.TestCase): |
3073 | 8 | """ ensure that the tree is pyflakes clean """ | 7 | """ ensure that the tree is pyflakes clean """ |
3074 | 9 | 8 | ||
3075 | diff --git a/tests/test_shortcuts.py b/tests/test_shortcuts.py | |||
3076 | index 1338e25..bf6651d 100644 | |||
3077 | --- a/tests/test_shortcuts.py | |||
3078 | +++ b/tests/test_shortcuts.py | |||
3079 | @@ -1,34 +1,106 @@ | |||
3081 | 1 | #!/usr/bin/python | 1 | #!/usr/bin/python3 |
3082 | 2 | 2 | ||
3083 | 3 | import apt | 3 | import apt |
3084 | 4 | 4 | ||
3085 | 5 | import unittest | 5 | import unittest |
3086 | 6 | import sys | 6 | import sys |
3096 | 7 | try: | 7 | import os |
3097 | 8 | from urllib.request import urlopen | 8 | |
3098 | 9 | from urllib.error import HTTPError, URLError | 9 | from aptsources.distro import get_distro |
3099 | 10 | except ImportError: | 10 | from aptsources.sourceslist import SourceEntry |
3100 | 11 | from urllib2 import HTTPError, URLError, urlopen | 11 | from contextlib import contextmanager |
3101 | 12 | try: | 12 | from http.client import HTTPException |
3102 | 13 | from http.client import HTTPException | 13 | from launchpadlib.launchpad import Launchpad |
3103 | 14 | except ImportError: | 14 | from mock import (patch, Mock) |
3104 | 15 | from httplib import HTTPException | 15 | from urllib.request import urlopen |
3105 | 16 | from urllib.error import URLError | ||
3106 | 16 | 17 | ||
3107 | 17 | sys.path.insert(0, "..") | 18 | sys.path.insert(0, "..") |
3108 | 18 | 19 | ||
3112 | 19 | from softwareproperties.SoftwareProperties import shortcut_handler | 20 | from softwareproperties.sourceslist import SourcesListShortcutHandler |
3113 | 20 | from softwareproperties.shortcuts import ShortcutException | 21 | from softwareproperties.uri import URIShortcutHandler |
3114 | 21 | from mock import patch | 22 | from softwareproperties.cloudarchive import CloudArchiveShortcutHandler |
3115 | 23 | from softwareproperties.ppa import PPAShortcutHandler | ||
3116 | 24 | from softwareproperties.shortcuthandler import InvalidShortcutException | ||
3117 | 25 | from softwareproperties.shortcuts import shortcut_handler | ||
3118 | 26 | |||
3119 | 27 | |||
3120 | 28 | DISTRO = get_distro() | ||
3121 | 29 | CODENAME = DISTRO.codename | ||
3122 | 30 | |||
3123 | 31 | # These must match the ppa used in the VALID_PPAS | ||
3124 | 32 | PPA_LINE = f"deb http://ppa.launchpad.net/ddstreet/ppa/ubuntu/ {CODENAME} main" | ||
3125 | 33 | PPA_FILEBASE = "ddstreet-ubuntu-ppa" | ||
3126 | 34 | PPA_SOURCEFILE = f"{PPA_FILEBASE}-{CODENAME}.list" | ||
3127 | 35 | PPA_TRUSTEDFILE = f"{PPA_FILEBASE}.gpg" | ||
3128 | 36 | PPA_NETRCFILE = f"{PPA_FILEBASE}.conf" | ||
3129 | 37 | |||
3130 | 38 | PRIVATE_PPA_PASSWORD = "thisisnotarealpassword" | ||
3131 | 39 | PRIVATE_PPA_LINE = f"deb https://private-ppa.launchpad.net/ddstreet/ppa/ubuntu/ {CODENAME} main" | ||
3132 | 40 | PRIVATE_PPA_NETRCCONTENT = f"machine private-ppa.launchpad.net/ddstreet/ppa/ubuntu/ login ddstreet password {PRIVATE_PPA_PASSWORD}" | ||
3133 | 41 | PRIVATE_PPA_SUBSCRIPTION_URLS = [f"https://ddstreet:{PRIVATE_PPA_PASSWORD}@private-ppa.launchpad.net/ddstreet/ppa/ubuntu/"] | ||
3134 | 42 | |||
3135 | 43 | # These must match the uca used in VALID_UCAS | ||
3136 | 44 | UCA_CANAME = "train" | ||
3137 | 45 | UCA_ARCHIVE = "http://ubuntu-cloud.archive.canonical.com/ubuntu" | ||
3138 | 46 | UCA_LINE = f"deb {UCA_ARCHIVE} bionic-updates/{UCA_CANAME} main" | ||
3139 | 47 | UCA_LINE_PROPOSED = f"deb {UCA_ARCHIVE} bionic-proposed/{UCA_CANAME} main" | ||
3140 | 48 | UCA_FILEBASE = f"cloudarchive-{UCA_CANAME}" | ||
3141 | 49 | UCA_SOURCEFILE = f"{UCA_FILEBASE}.list" | ||
3142 | 50 | CA_ALLOW_CODENAME = "bionic" | ||
3143 | 51 | |||
3144 | 52 | # This must match the VALID_URIS | ||
3145 | 53 | URI = "http://fake.mirror.private.com/ubuntu" | ||
3146 | 54 | URI_FILEBASE = f"archive_uri-http_fake_mirror_private_com_ubuntu" | ||
3147 | 55 | URI_SOURCEFILE = f"{URI_FILEBASE}-{CODENAME}.list" | ||
3148 | 56 | |||
3149 | 57 | VALID_LINES = [f"deb {URI} bionic main"] | ||
3150 | 58 | VALID_URIS = [URI] | ||
3151 | 59 | VALID_PPAS = ["ppa:ddstreet", "ppa:~ddstreet", "ppa:ddstreet/ppa", "ppa:~ddstreet/ppa", "ppa:ddstreet/ubuntu/ppa", "ppa:~ddstreet/ubuntu/ppa"] | ||
3152 | 60 | VALID_UCAS = [f"cloud-archive:{UCA_CANAME}", f"cloud-archive:{UCA_CANAME}-updates", f"cloud-archive:{UCA_CANAME}-proposed", f"uca:{UCA_CANAME}", f"uca:{UCA_CANAME}-updates", f"uca:{UCA_CANAME}-proposed"] | ||
3153 | 61 | VALID_ALL = VALID_LINES + VALID_URIS + VALID_PPAS + VALID_UCAS | ||
3154 | 62 | |||
3155 | 63 | INVALID_LINES = ["xxx invalid deb line"] | ||
3156 | 64 | INVALID_URIS = ["invalid"] | ||
3157 | 65 | INVALID_PPAS = ["ppainvalid:ddstreet", "ppa:ddstreet/ubuntu/ppa/invalid"] | ||
3158 | 66 | INVALID_UCAS = [f"cloud-invalid:{UCA_CANAME}", "cloud-archive:"] | ||
3159 | 67 | INVALID_ALL = INVALID_LINES + INVALID_URIS + INVALID_PPAS + INVALID_UCAS | ||
3160 | 22 | 68 | ||
3161 | 23 | def has_network(): | 69 | def has_network(): |
3162 | 24 | try: | 70 | try: |
3165 | 25 | network = urlopen("https://launchpad.net/") | 71 | with urlopen("https://launchpad.net/"): |
3166 | 26 | network | 72 | pass |
3167 | 27 | except (URLError, HTTPException): | 73 | except (URLError, HTTPException): |
3168 | 28 | return False | 74 | return False |
3169 | 29 | return True | 75 | return True |
3170 | 30 | 76 | ||
3171 | 77 | def mock_login_with(*args, **kwargs): | ||
3172 | 78 | _lp = Launchpad.login_anonymously(*args, **kwargs) | ||
3173 | 79 | lp = Mock(wraps=_lp) | ||
3174 | 80 | |||
3175 | 81 | lp.me = Mock() | ||
3176 | 82 | lp.me.name = 'ddstreet' | ||
3177 | 83 | lp.me.getArchiveSubscriptionURLs = lambda: PRIVATE_PPA_SUBSCRIPTION_URLS | ||
3178 | 84 | |||
3179 | 85 | def mock_getPPAByName(_team, name): | ||
3180 | 86 | _ppa = _team.getPPAByName(name=name) | ||
3181 | 87 | ppa = Mock(wraps=_ppa) | ||
3182 | 88 | ppa.signing_key_fingerprint = _ppa.signing_key_fingerprint | ||
3183 | 89 | ppa.private = True | ||
3184 | 90 | return ppa | ||
3185 | 91 | |||
3186 | 92 | def mock_people(teamname): | ||
3187 | 93 | _team = _lp.people(teamname) | ||
3188 | 94 | team = Mock(wraps=_team) | ||
3189 | 95 | team.getPPAByName = lambda name: mock_getPPAByName(_team, name) | ||
3190 | 96 | return team | ||
3191 | 97 | |||
3192 | 98 | lp.people = mock_people | ||
3193 | 99 | return lp | ||
3194 | 100 | |||
3195 | 101 | |||
3196 | 31 | class ShortcutsTestcase(unittest.TestCase): | 102 | class ShortcutsTestcase(unittest.TestCase): |
3197 | 103 | enable_source = False | ||
3198 | 32 | 104 | ||
3199 | 33 | @classmethod | 105 | @classmethod |
3200 | 34 | def setUpClass(cls): | 106 | def setUpClass(cls): |
3201 | @@ -42,37 +114,133 @@ class ShortcutsTestcase(unittest.TestCase): | |||
3202 | 42 | apt.apt_pkg.config.set("Dir::Etc", "etc/apt") | 114 | apt.apt_pkg.config.set("Dir::Etc", "etc/apt") |
3203 | 43 | apt.apt_pkg.config.set("Dir::Etc::sourcelist", "sources.list") | 115 | apt.apt_pkg.config.set("Dir::Etc::sourcelist", "sources.list") |
3204 | 44 | apt.apt_pkg.config.set("Dir::Etc::sourceparts", "sources.list.d") | 116 | apt.apt_pkg.config.set("Dir::Etc::sourceparts", "sources.list.d") |
3205 | 117 | apt.apt_pkg.config.set("Dir::Etc::trustedparts", "trusted.gpg.d") | ||
3206 | 118 | apt.apt_pkg.config.set("Dir::Etc::netrcparts", "auth.conf.d") | ||
3207 | 119 | |||
3208 | 120 | def create_handler(self, line, handler, *args, **kwargs): | ||
3209 | 121 | return handler(line, *args, enable_source=self.enable_source, **kwargs) | ||
3210 | 45 | 122 | ||
3215 | 46 | def test_shortcut_none(self): | 123 | def create_handlers(self, line, handler, *args, **kwargs): |
3216 | 47 | line = "deb http://ubuntu.com/ubuntu trusty main" | 124 | handlers = handler if isinstance(handler, list) else [handler] |
3217 | 48 | handler = shortcut_handler(line) | 125 | # note, always appends shortcut_handler |
3218 | 49 | self.assertEqual((line, None), handler.expand()) | 126 | return [self.create_handler(line, handler, *args, **kwargs) |
3219 | 127 | for handler in handlers + [shortcut_handler]] | ||
3220 | 128 | |||
3221 | 129 | def check_shortcut(self, shortcut, line, sourcefile=None, trustedfile=None, netrcfile=None, | ||
3222 | 130 | sourceparts=apt.apt_pkg.config.find_dir("Dir::Etc::sourceparts"), | ||
3223 | 131 | trustedparts=apt.apt_pkg.config.find_dir("Dir::Etc::trustedparts"), | ||
3224 | 132 | netrcparts=apt.apt_pkg.config.find_dir("Dir::Etc::netrcparts"), | ||
3225 | 133 | trustedcontent=False, netrccontent=None): | ||
3226 | 134 | self.assertEqual(shortcut.SourceEntry().line, line) | ||
3227 | 135 | |||
3228 | 136 | self.assertEqual(shortcut.sourceparts_path, sourceparts) | ||
3229 | 137 | if sourcefile: | ||
3230 | 138 | self.assertEqual(shortcut.SourceEntry().file, os.path.join(sourceparts, sourcefile)) | ||
3231 | 139 | self.assertEqual(shortcut.sourceparts_filename, sourcefile) | ||
3232 | 140 | self.assertEqual(shortcut.sourceparts_file, os.path.join(sourceparts, sourcefile)) | ||
3233 | 141 | |||
3234 | 142 | binentry = SourceEntry(line) | ||
3235 | 143 | binentry.type = DISTRO.binary_type | ||
3236 | 144 | self.assertEqual(shortcut.SourceEntry(shortcut.binary_type), binentry) | ||
3237 | 145 | |||
3238 | 146 | srcentry = SourceEntry(line) | ||
3239 | 147 | srcentry.type = DISTRO.source_type | ||
3240 | 148 | srcentry.set_enabled(self.enable_source) | ||
3241 | 149 | self.assertEqual(shortcut.SourceEntry(shortcut.source_type), srcentry) | ||
3242 | 150 | |||
3243 | 151 | self.assertEqual(shortcut.trustedparts_path, trustedparts) | ||
3244 | 152 | if trustedfile: | ||
3245 | 153 | self.assertEqual(shortcut.trustedparts_filename, trustedfile) | ||
3246 | 154 | self.assertEqual(shortcut.trustedparts_file, os.path.join(trustedparts, trustedfile)) | ||
3247 | 155 | |||
3248 | 156 | # Checking the actual gpg key content is too much work. | ||
3249 | 157 | if trustedcontent: | ||
3250 | 158 | self.assertIsNotNone(shortcut.trustedparts_content) | ||
3251 | 159 | else: | ||
3252 | 160 | self.assertIsNone(shortcut.trustedparts_content) | ||
3253 | 161 | |||
3254 | 162 | self.assertEqual(shortcut.netrcparts_path, netrcparts) | ||
3255 | 163 | if netrcfile: | ||
3256 | 164 | self.assertEqual(shortcut.netrcparts_filename, netrcfile) | ||
3257 | 165 | self.assertEqual(shortcut.netrcparts_file, os.path.join(netrcparts, netrcfile)) | ||
3258 | 166 | |||
3259 | 167 | self.assertEqual(shortcut.netrcparts_content, netrccontent) | ||
3260 | 168 | |||
3261 | 169 | def test_shortcut_sourceslist(self): | ||
3262 | 170 | for line in VALID_LINES: | ||
3263 | 171 | for shortcut in self.create_handlers(line, SourcesListShortcutHandler): | ||
3264 | 172 | self.check_shortcut(shortcut, line) | ||
3265 | 173 | |||
3266 | 174 | def test_shortcut_uri(self): | ||
3267 | 175 | for uri in VALID_URIS: | ||
3268 | 176 | line = f"deb {uri} {CODENAME} main" | ||
3269 | 177 | for shortcut in self.create_handlers(uri, URIShortcutHandler): | ||
3270 | 178 | self.check_shortcut(shortcut, line, sourcefile=URI_SOURCEFILE) | ||
3271 | 50 | 179 | ||
3272 | 51 | @unittest.skipUnless(has_network(), "requires network") | 180 | @unittest.skipUnless(has_network(), "requires network") |
3273 | 52 | def test_shortcut_ppa(self): | 181 | def test_shortcut_ppa(self): |
3280 | 53 | line = "ppa:mvo" | 182 | for ppa in VALID_PPAS: |
3281 | 54 | handler = shortcut_handler(line) | 183 | for shortcut in self.create_handlers(ppa, PPAShortcutHandler): |
3282 | 55 | self.assertEqual( | 184 | self.check_shortcut(shortcut, PPA_LINE, |
3283 | 56 | ('deb http://ppa.launchpad.net/mvo/ppa/ubuntu trusty main', | 185 | sourcefile=PPA_SOURCEFILE, |
3284 | 57 | '/etc/apt/sources.list.d/mvo-ubuntu-ppa-trusty.list'), | 186 | trustedfile=PPA_TRUSTEDFILE, |
3285 | 58 | handler.expand("trusty", distro="ubuntu")) | 187 | netrcfile=PPA_NETRCFILE, |
3286 | 188 | trustedcontent=True) | ||
3287 | 59 | 189 | ||
3288 | 60 | @unittest.skipUnless(has_network(), "requires network") | 190 | @unittest.skipUnless(has_network(), "requires network") |
3289 | 191 | def test_shortcut_private_ppa(self): | ||
3290 | 192 | # this is the same tests as the public ppa, but login=True will use the mocked lp instance | ||
3291 | 193 | # this *does not* actually test/verify this works with a real private ppa; that must be done manually | ||
3292 | 194 | with patch('launchpadlib.launchpad.Launchpad.login_with', new=mock_login_with): | ||
3293 | 195 | for ppa in VALID_PPAS: | ||
3294 | 196 | for shortcut in self.create_handlers(ppa, PPAShortcutHandler, login=True): | ||
3295 | 197 | self.check_shortcut(shortcut, PRIVATE_PPA_LINE, | ||
3296 | 198 | sourcefile=PPA_SOURCEFILE, | ||
3297 | 199 | trustedfile=PPA_TRUSTEDFILE, | ||
3298 | 200 | netrcfile=PPA_NETRCFILE, | ||
3299 | 201 | trustedcontent=True, | ||
3300 | 202 | netrccontent=PRIVATE_PPA_NETRCCONTENT) | ||
3301 | 203 | |||
3302 | 204 | @contextmanager | ||
3303 | 205 | def ca_allow_codename(self, codename): | ||
3304 | 206 | key = "CA_ALLOW_CODENAME" | ||
3305 | 207 | orig = os.environ.get(key, None) | ||
3306 | 208 | try: | ||
3307 | 209 | os.environ[key] = codename | ||
3308 | 210 | yield | ||
3309 | 211 | finally: | ||
3310 | 212 | if orig: | ||
3311 | 213 | os.environ[key] = orig | ||
3312 | 214 | else: | ||
3313 | 215 | os.environ.pop(key, None) | ||
3314 | 216 | |||
3315 | 61 | def test_shortcut_cloudarchive(self): | 217 | def test_shortcut_cloudarchive(self): |
3329 | 62 | line = "cloud-archive:folsom" | 218 | for uca in VALID_UCAS: |
3330 | 63 | handler = shortcut_handler(line) | 219 | line = UCA_LINE_PROPOSED if 'proposed' in uca else UCA_LINE |
3331 | 64 | self.assertEqual( | 220 | with self.ca_allow_codename(CA_ALLOW_CODENAME): |
3332 | 65 | ('deb http://ubuntu-cloud.archive.canonical.com/ubuntu '\ | 221 | for shortcut in self.create_handlers(uca, CloudArchiveShortcutHandler): |
3333 | 66 | 'precise-updates/folsom main', | 222 | self.check_shortcut(shortcut, line, sourcefile=UCA_SOURCEFILE) |
3334 | 67 | '/etc/apt/sources.list.d/cloudarchive-folsom.list'), | 223 | |
3335 | 68 | handler.expand("precise", distro="ubuntu")) | 224 | def check_invalid_shortcut(self, handler, shortcut): |
3336 | 69 | 225 | msg = "'%s' should have rejected '%s'" % (handler, shortcut) | |
3337 | 70 | def test_shortcut_exception(self): | 226 | with self.assertRaises(InvalidShortcutException, msg=msg): |
3338 | 71 | with self.assertRaises(ShortcutException): | 227 | self.create_handler(shortcut, handler) |
3339 | 72 | with patch('softwareproperties.ppa.get_ppa_info_from_lp', | 228 | |
3340 | 73 | side_effect=lambda *args: HTTPError("url", 404, "not found", [], None)): | 229 | def test_shortcut_invalid(self): |
3341 | 74 | shortcut_handler("ppa:mvo") | 230 | for s in INVALID_ALL + VALID_URIS + VALID_PPAS + VALID_UCAS: |
3342 | 231 | self.check_invalid_shortcut(SourcesListShortcutHandler, s) | ||
3343 | 232 | for s in INVALID_ALL + VALID_LINES + VALID_PPAS + VALID_UCAS: | ||
3344 | 233 | self.check_invalid_shortcut(URIShortcutHandler, s) | ||
3345 | 234 | for s in INVALID_ALL + VALID_LINES + VALID_URIS + VALID_UCAS: | ||
3346 | 235 | self.check_invalid_shortcut(PPAShortcutHandler, s) | ||
3347 | 236 | for s in INVALID_ALL + VALID_LINES + VALID_URIS + VALID_PPAS: | ||
3348 | 237 | self.check_invalid_shortcut(CloudArchiveShortcutHandler, s) | ||
3349 | 238 | for s in INVALID_ALL: | ||
3350 | 239 | self.check_invalid_shortcut(shortcut_handler, s) | ||
3351 | 240 | |||
3352 | 75 | 241 | ||
3353 | 242 | class EnableSourceShortcutsTestcase(ShortcutsTestcase): | ||
3354 | 243 | enable_source = True | ||
3355 | 76 | 244 | ||
3356 | 77 | 245 | ||
3357 | 78 | if __name__ == "__main__": | 246 | if __name__ == "__main__": |
3358 | diff --git a/tests/test_sp.py b/tests/test_sp.py | |||
3359 | index b372f66..f6bc8fc 100644 | |||
3360 | --- a/tests/test_sp.py | |||
3361 | +++ b/tests/test_sp.py | |||
3362 | @@ -1,4 +1,4 @@ | |||
3364 | 1 | #!/usr/bin/python | 1 | #!/usr/bin/python3 |
3365 | 2 | # -*- coding: utf-8 -*- | 2 | # -*- coding: utf-8 -*- |
3366 | 3 | 3 | ||
3367 | 4 | import apt_pkg | 4 | import apt_pkg |
RFC, in-progress git repo