Merge ~adam-collard/layer-snap:add-snap-proxy into ~stub/layer-snap:master

Proposed by Adam Collard
Status: Merged
Merge reported by: Stuart Bishop
Merged at revision: 969221e73efbd6b246aa5e2c5c8427b607bc8bcd
Proposed branch: ~adam-collard/layer-snap:add-snap-proxy
Merge into: ~stub/layer-snap:master
Diff against target: 174 lines (+105/-3)
3 files modified
config.yaml (+6/-0)
reactive/__init__.py (+0/-0)
reactive/snap.py (+99/-3)
Reviewer Review Type Date Requested Status
Stuart Bishop Approve
Review via email: mp+336289@code.launchpad.net

Commit message

Add support for snap proxy

Description of the change

Add support for a Snap Enterprise Proxy.

A new config option, snap_proxy_url, has been added to point to an Enterprise Proxy. Units will then configure their snapd to talk to the Enteprise Proxy instead of directly to the upstream store.

Support for http_proxy has been retained but the configuration options should be considered mutually exclusive.

To post a comment you must log in.
Revision history for this message
Stuart Bishop (stub) wrote :

Some imports need to move to keep this layer cross platform, and we need to handle the snap_proxy_url config setting changing. I'm not familiar with the proxy though to know how practical that is, or how to handle multiple stores being configured.

review: Needs Fixing
Revision history for this message
Adam Collard (adam-collard) wrote :

Sorry for the delay in addressing this feedback, now done.

Revision history for this message
Stuart Bishop (stub) wrote :

Some minor quibbles. This is landable, but I'd like to get changing the proxy url working or document in the config that it cannot be changed post deploy.

Revision history for this message
Adam Collard (adam-collard) :
Revision history for this message
Stuart Bishop (stub) wrote :

Is good

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/config.yaml b/config.yaml
2index 1271e30..754d46a 100644
3--- a/config.yaml
4+++ b/config.yaml
5@@ -19,3 +19,9 @@ options:
6 HTTP/HTTPS web proxy for Snappy to use when accessing the snap store.
7 type: string
8 default: ""
9+ snap_proxy_url:
10+ default: ""
11+ type: string
12+ description: |
13+ The address of a Snappy Enterprise Proxy to use for snaps
14+ e.g. http://snap-proxy.example.com
15diff --git a/reactive/__init__.py b/reactive/__init__.py
16new file mode 100644
17index 0000000..e69de29
18--- /dev/null
19+++ b/reactive/__init__.py
20diff --git a/reactive/snap.py b/reactive/snap.py
21index 90895d3..884dc63 100644
22--- a/reactive/snap.py
23+++ b/reactive/snap.py
24@@ -16,22 +16,40 @@
25 '''
26 charms.reactive helpers for dealing with Snap packages.
27 '''
28+from distutils.version import LooseVersion
29 import os.path
30 from os import uname
31 import shutil
32 import subprocess
33 from textwrap import dedent
34 import time
35+from urllib.request import urlretrieve
36
37 from charmhelpers.core import hookenv, host
38 from charmhelpers.core.hookenv import ERROR
39+from charmhelpers.core.host import write_file
40 from charms import layer
41 from charms import reactive
42 from charms.layer import snap
43-from charms.reactive import hook
44 from charms.reactive.helpers import data_changed
45
46
47+class UnsatisfiedMinimumVersionError(Exception):
48+
49+ def __init__(self, desired, actual):
50+ super().__init__()
51+ self.desired = desired
52+ self.actual = actual
53+
54+ def __str__(self):
55+ return "Could not install snapd >= {0.desired}, got {0.actual}".format(
56+ self)
57+
58+
59+class InvalidBundleError(Exception):
60+ pass
61+
62+
63 def install():
64 opts = layer.options('snap')
65 # supported-architectures is EXPERIMENTAL and undocumented.
66@@ -68,7 +86,7 @@ def refresh():
67 snap.connect_all()
68
69
70-@hook('upgrade-charm')
71+@reactive.hook('upgrade-charm')
72 def upgrade_charm():
73 refresh()
74
75@@ -88,7 +106,7 @@ def snapd_supported():
76 def ensure_snapd():
77 if not snapd_supported():
78 hookenv.log('Snaps do not work in this environment', hookenv.ERROR)
79- return
80+ raise Exception('Snaps do not work in this environment')
81
82 # I don't use the apt layer, because that would tie this layer
83 # too closely to apt packaging. Perhaps this is a snap-only system.
84@@ -168,6 +186,83 @@ def ensure_path():
85 os.environ['PATH'] += ':/snap/bin'
86
87
88+def _get_snapd_version():
89+ stdout = subprocess.check_output(
90+ ['snap', 'version'],
91+ stdin=subprocess.DEVNULL,
92+ universal_newlines=True
93+ )
94+ version_info = dict(line.split() for line in stdout.splitlines())
95+ return LooseVersion(version_info['snapd'])
96+
97+
98+PREFERENCES = """\
99+Package: *
100+Pin: release a={}-proposed
101+Pin-Priority: 400
102+"""
103+
104+
105+def ensure_snapd_min_version(min_version):
106+ snapd_version = _get_snapd_version()
107+ if snapd_version < LooseVersion(min_version):
108+ from charmhelpers.fetch import add_source, apt_update, apt_install
109+ # Temporary until LP:1735344 lands
110+ add_source('distro-proposed', fail_invalid=True)
111+ distro = get_series()
112+ # disable proposed by default, needs to explicit
113+ write_file(
114+ '/etc/apt/preferences.d/proposed',
115+ PREFERENCES.format(distro),
116+ )
117+ apt_update()
118+ # explicitly install snapd from proposed
119+ apt_install('snapd/{}-proposed'.format(distro))
120+ snapd_version = _get_snapd_version()
121+ if snapd_version < LooseVersion(min_version):
122+ hookenv.log(
123+ "Failed to install snapd >= {}".format(min_version), ERROR)
124+ raise UnsatisfiedMinimumVersionError(min_version, snapd_version)
125+
126+
127+def download_assertion_bundle(proxy_url):
128+ """Download proxy assertion bundle and store id"""
129+ assertions_url = '{}/v2/auth/store/assertions'.format(proxy_url)
130+ local_bundle, headers = urlretrieve(assertions_url)
131+ store_id = headers['X-Assertion-Store-Id']
132+ return local_bundle, store_id
133+
134+
135+def configure_snap_enterprise_proxy():
136+ if not reactive.is_flag_set('config.changed.snap_proxy_url'):
137+ return
138+ ensure_snapd_min_version('2.30')
139+ enterprise_proxy_url = hookenv.config()['snap_proxy_url']
140+ if enterprise_proxy_url:
141+ bundle, store_id = download_assertion_bundle(enterprise_proxy_url)
142+ try:
143+ subprocess.check_output(
144+ ['snap', 'ack', bundle],
145+ stdin=subprocess.DEVNULL,
146+ universal_newlines=True,
147+ )
148+ except subprocess.CalledProcessError as e:
149+ raise InvalidBundleError(
150+ 'snapd could not ack the proxy assertion: ' + e.output)
151+ else:
152+ store_id = ''
153+
154+ try:
155+ subprocess.check_output(
156+ ['snap', 'set', 'core', 'proxy.store={}'.format(store_id)],
157+ stdin=subprocess.DEVNULL,
158+ universal_newlines=True,
159+ )
160+ except subprocess.CalledProcessError as e:
161+ raise InvalidBundleError(
162+ 'Proxy ID from header did not match store assertion: ' + e.output)
163+
164+
165 # Per https://github.com/juju-solutions/charms.reactive/issues/33,
166 # this module may be imported multiple times so ensure the
167 # initialization hook is only registered once. I have to piggy back
168@@ -183,5 +278,6 @@ if not hasattr(reactive, '_snap_registered'):
169 hookenv.atstart(ensure_snapd)
170 hookenv.atstart(ensure_path)
171 hookenv.atstart(update_snap_proxy)
172+ hookenv.atstart(configure_snap_enterprise_proxy)
173 hookenv.atstart(install)
174 reactive._snap_registered = True

Subscribers

People subscribed via source and target branches

to all changes: