Status: | Merged |
---|---|
Approved by: | Simon C |
Approved revision: | no longer in the source branch. |
Merged at revision: | 4 |
Proposed branch: | lp:~slimey/engage/stockwell |
Merge into: | lp:engage |
Diff against target: |
816 lines (+464/-162) 21 files modified
.bzrignore (+2/-0) bin/engage (+28/-0) debian/changelog (+6/-0) debian/control (+2/-3) debian/engage.install (+3/-0) debian/postinst (+25/-0) etc/init/engage.conf (+1/-1) lib-depends/get-egg-src (+40/-0) restart (+0/-2) setup.py (+3/-3) src/engage/media/__init__.py (+91/-0) src/engage/media/movie.py (+21/-0) src/engage/media/static.py (+36/-0) src/engage/monitor/percent_used.py (+3/-3) src/engage/ui/carousel.py (+37/-48) src/engage/ui/web.py (+2/-0) src/engage/util/atv.py (+94/-0) src/engage/util/twisted_plugin.py (+65/-0) src/twisted/plugins/engage_plugin.py (+5/-66) start (+0/-19) stop (+0/-17) |
To merge this branch: | bzr merge lp:~slimey/engage/stockwell |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Simon C | Approve | ||
Review via email: mp+126278@code.launchpad.net |
Commit message
Embedded Twisted 12 and further ATV support
Description of the change
- embedded twisted 12 for deployment on ubuntu
- further ATV support (video support still half-baked but harmless)
To post a comment you must log in.
Revision history for this message
Simon C (slimey) : | # |
review:
Approve
lp:~slimey/engage/stockwell
updated
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file '.bzrignore' | |||
2 | --- .bzrignore 1970-01-01 00:00:00 +0000 | |||
3 | +++ .bzrignore 2012-09-25 15:49:24 +0000 | |||
4 | @@ -0,0 +1,2 @@ | |||
5 | 1 | dropin.cache | ||
6 | 2 | twisted-engage.egg | ||
7 | 0 | 3 | ||
8 | === added directory 'bin' | |||
9 | === added file 'bin/engage' | |||
10 | --- bin/engage 1970-01-01 00:00:00 +0000 | |||
11 | +++ bin/engage 2012-09-25 15:49:24 +0000 | |||
12 | @@ -0,0 +1,28 @@ | |||
13 | 1 | #!/usr/bin/python | ||
14 | 2 | # | ||
15 | 3 | # This is just a crazy equivalent of | ||
16 | 4 | # | ||
17 | 5 | # /usr/bin/twistd --uid nobody --gid nogroup --logfile /var/log/engage.log --nodaemon engage | ||
18 | 6 | # | ||
19 | 7 | # but allowing Twisted to be embedded, for those ubutnu machines with | ||
20 | 8 | # a stock (pre-12.0) version. | ||
21 | 9 | # | ||
22 | 10 | # Happy day when we can ditch this. | ||
23 | 11 | # | ||
24 | 12 | import os, platform, sys | ||
25 | 13 | try: | ||
26 | 14 | import _preamble | ||
27 | 15 | except ImportError: | ||
28 | 16 | sys.exc_clear() | ||
29 | 17 | |||
30 | 18 | lib_dir = '/usr/lib/engage/lib-depends' | ||
31 | 19 | sys.path.insert(0, lib_dir) | ||
32 | 20 | sys.path.insert(0, os.path.join(lib_dir, 'twisted-engage.egg')) | ||
33 | 21 | |||
34 | 22 | argv_head = sys.argv[0] | ||
35 | 23 | argv_tail = sys.argv[1:] | ||
36 | 24 | sys.argv = [argv_head, '--uid', 'nobody', '--gid', 'nogroup', '--logfile', | ||
37 | 25 | '/var/log/engage.log', '--nodaemon'] + argv_tail + ['engage'] | ||
38 | 26 | |||
39 | 27 | from twisted.scripts.twistd import run | ||
40 | 28 | run() | ||
41 | 0 | 29 | ||
42 | === modified file 'debian/changelog' | |||
43 | --- debian/changelog 2012-09-17 13:13:06 +0000 | |||
44 | +++ debian/changelog 2012-09-25 15:49:24 +0000 | |||
45 | @@ -1,3 +1,9 @@ | |||
46 | 1 | engage (0.6.1) precise; urgency=low | ||
47 | 2 | |||
48 | 3 | * Embedded twisted 12 for easy of installing on stock ubuntu | ||
49 | 4 | |||
50 | 5 | -- Simon C <simonc@ensoft.co.uk> Mon, 24 Sep 2012 22:42:58 +0100 | ||
51 | 6 | |||
52 | 1 | engage (0.6) lucid; urgency=low | 7 | engage (0.6) lucid; urgency=low |
53 | 2 | 8 | ||
54 | 3 | * Improved structure and added basic ATV support | 9 | * Improved structure and added basic ATV support |
55 | 4 | 10 | ||
56 | === modified file 'debian/control' | |||
57 | --- debian/control 2012-09-17 13:13:06 +0000 | |||
58 | +++ debian/control 2012-09-25 15:49:24 +0000 | |||
59 | @@ -8,8 +8,7 @@ | |||
60 | 8 | Package: engage | 8 | Package: engage |
61 | 9 | Architecture: all | 9 | Architecture: all |
62 | 10 | # snmp package is just for snmptranslate | 10 | # snmp package is just for snmptranslate |
64 | 11 | Depends: python,python-twisted,python-twisted-web (>= 12.0.0),python-rrdtool,libsnmp-python,python-pynetsnmp,snmp,snmp-mibs-downloader,${misc:Depends} | 11 | Depends: python (>= ${python:Versions}), python-setuptools, python-zope.interface, python-rrdtool, libsnmp-python, python-pynetsnmp, snmp, snmp-mibs-downloader, ${python:Depends}, ${misc:Depends} |
65 | 12 | Description: Combined system/network statistics and Apple TV control framework | 12 | Description: Combined system/network statistics and Apple TV control framework |
66 | 13 | EnGage is a plugin-based framework for gathering, summarizing, and displaying | 13 | EnGage is a plugin-based framework for gathering, summarizing, and displaying |
69 | 14 | system or network administration data. This base package provides basic | 14 | system or network administration data. |
68 | 15 | monitoring of the linux host itself. | ||
70 | 16 | 15 | ||
71 | === modified file 'debian/engage.install' | |||
72 | --- debian/engage.install 2012-09-17 13:13:06 +0000 | |||
73 | +++ debian/engage.install 2012-09-25 15:49:24 +0000 | |||
74 | @@ -1,3 +1,6 @@ | |||
75 | 1 | etc/engage.conf etc | 1 | etc/engage.conf etc |
76 | 2 | etc/init/engage.conf etc/init | 2 | etc/init/engage.conf etc/init |
77 | 3 | static/ usr/share/engage | 3 | static/ usr/share/engage |
78 | 4 | bin/engage /usr/sbin | ||
79 | 5 | lib-depends/twisted-engage-egg-src.tar.bz2 /usr/lib/engage/lib-depends | ||
80 | 6 | lib-depends/twisted-engage.egg /usr/lib/engage/lib-depends | ||
81 | 4 | 7 | ||
82 | === added file 'debian/postinst' | |||
83 | --- debian/postinst 1970-01-01 00:00:00 +0000 | |||
84 | +++ debian/postinst 2012-09-25 15:49:24 +0000 | |||
85 | @@ -0,0 +1,25 @@ | |||
86 | 1 | #!/bin/sh | ||
87 | 2 | # | ||
88 | 3 | # Build the embedded Twisted 12.2 package into an egg for the target system | ||
89 | 4 | # architecture. When it's reasonable to just put Twisted >= 12.0 as a | ||
90 | 5 | # dependency and expect ubuntu to honour it, we can cheerfully flush this down | ||
91 | 6 | # the can. | ||
92 | 7 | # | ||
93 | 8 | # We include a zero-byte twisted-engage.egg and mark it as a conffile, so it's | ||
94 | 9 | # ok to overwrite it here with a real egg. | ||
95 | 10 | # | ||
96 | 11 | set -e | ||
97 | 12 | egg_src=/usr/lib/engage/lib-depends/twisted-engage-egg-src.tar.bz2 | ||
98 | 13 | egg=/usr/lib/engage/lib-depends/twisted-engage.egg | ||
99 | 14 | tmp=/tmp/engage-$$ | ||
100 | 15 | rm -rf $tmp | ||
101 | 16 | mkdir $tmp | ||
102 | 17 | cd $tmp | ||
103 | 18 | tar jxf $egg_src | ||
104 | 19 | cd Twisted | ||
105 | 20 | python setup.py bdist_egg | ||
106 | 21 | mv dist/*.egg $egg | ||
107 | 22 | cd /tmp | ||
108 | 23 | rm -rf $tmp | ||
109 | 24 | |||
110 | 25 | #DEBHELPER# | ||
111 | 0 | 26 | ||
112 | === modified file 'etc/init/engage.conf' | |||
113 | --- etc/init/engage.conf 2012-09-18 21:21:43 +0000 | |||
114 | +++ etc/init/engage.conf 2012-09-25 15:49:24 +0000 | |||
115 | @@ -5,5 +5,5 @@ | |||
116 | 5 | stop on stopping network | 5 | stop on stopping network |
117 | 6 | stop on starting shutdown | 6 | stop on starting shutdown |
118 | 7 | 7 | ||
120 | 8 | exec /usr/bin/twistd --uid nobody --gid nogroup --logfile /var/log/engage.log --nodaemon engage | 8 | exec /usr/sbin/engage |
121 | 9 | respawn | 9 | respawn |
122 | 10 | 10 | ||
123 | === added directory 'lib-depends' | |||
124 | === added file 'lib-depends/get-egg-src' | |||
125 | --- lib-depends/get-egg-src 1970-01-01 00:00:00 +0000 | |||
126 | +++ lib-depends/get-egg-src 2012-09-25 15:49:24 +0000 | |||
127 | @@ -0,0 +1,40 @@ | |||
128 | 1 | #!/bin/sh | ||
129 | 2 | # | ||
130 | 3 | # Download twisted from pypi, add our own plugin, and then tar up so | ||
131 | 4 | # it can be built on each target build. | ||
132 | 5 | # | ||
133 | 6 | # When we can just impose twisted >= 12.0 as a dependency and expect a | ||
134 | 7 | # normal ubuntu to be able to honour that, we can trash this whole | ||
135 | 8 | # sorry effort. That includes: | ||
136 | 9 | # | ||
137 | 10 | # - Killing this file, the twisted egg source file, and the zero-byte | ||
138 | 11 | # twisted-engage.egg file in this directory that is present just for | ||
139 | 12 | # dpkg to keep track of. | ||
140 | 13 | # | ||
141 | 14 | # - Killing the engage binary and just running "twistd engage" from | ||
142 | 15 | # etc/init with appropriate options. | ||
143 | 16 | # | ||
144 | 17 | # - Remove python-setuptools from the dependencies in debian/control | ||
145 | 18 | # and add python-twisted-core and python-twisted-web. | ||
146 | 19 | # | ||
147 | 20 | # - Moving engage/util/twisted_plugin.py back to twisted/plugins. | ||
148 | 21 | # | ||
149 | 22 | # - Adding twisted.plugins back to setup.py. | ||
150 | 23 | # | ||
151 | 24 | # Simon C, September 2012 | ||
152 | 25 | |||
153 | 26 | set -e | ||
154 | 27 | src_target=$PWD/twisted-engage-egg-src.tar.bz2 | ||
155 | 28 | tmp=/tmp/engage-$$ | ||
156 | 29 | plugin_file=twisted/plugins/engage_plugin.py | ||
157 | 30 | plugin_src=$PWD/../src/$plugin_file | ||
158 | 31 | rm -rf $tmp | ||
159 | 32 | mkdir $tmp | ||
160 | 33 | cd $tmp | ||
161 | 34 | pip install --upgrade --no-install Twisted | ||
162 | 35 | cd build/Twisted | ||
163 | 36 | cp $plugin_src $plugin_file | ||
164 | 37 | (cd ..; tar cf - Twisted | bzip2 -9 > $src_target) | ||
165 | 38 | cd / | ||
166 | 39 | rm -rf $tmp | ||
167 | 40 | ls -l $src_target | ||
168 | 0 | 41 | ||
169 | === added file 'lib-depends/twisted-engage-egg-src.tar.bz2' | |||
170 | 1 | Binary files lib-depends/twisted-engage-egg-src.tar.bz2 1970-01-01 00:00:00 +0000 and lib-depends/twisted-engage-egg-src.tar.bz2 2012-09-25 15:49:24 +0000 differ | 42 | Binary files lib-depends/twisted-engage-egg-src.tar.bz2 1970-01-01 00:00:00 +0000 and lib-depends/twisted-engage-egg-src.tar.bz2 2012-09-25 15:49:24 +0000 differ |
171 | === removed file 'restart' | |||
172 | --- restart 2012-09-14 12:24:16 +0000 | |||
173 | +++ restart 1970-01-01 00:00:00 +0000 | |||
174 | @@ -1,2 +0,0 @@ | |||
175 | 1 | ./stop $* | ||
176 | 2 | ./start $* | ||
177 | 3 | 0 | ||
178 | === modified file 'setup.py' | |||
179 | --- setup.py 2012-09-18 21:21:43 +0000 | |||
180 | +++ setup.py 2012-09-25 15:49:24 +0000 | |||
181 | @@ -7,10 +7,10 @@ | |||
182 | 7 | from distutils.core import setup | 7 | from distutils.core import setup |
183 | 8 | 8 | ||
184 | 9 | setup(name='engage', | 9 | setup(name='engage', |
186 | 10 | version="0.6", | 10 | version="0.6.1", |
187 | 11 | description='EnGage: system/network statistics monitoring and Apple TV control', | 11 | description='EnGage: system/network statistics monitoring and Apple TV control', |
188 | 12 | url='http://open.ensoft.co.uk/EnGage', | 12 | url='http://open.ensoft.co.uk/EnGage', |
191 | 13 | packages=['engage', 'engage.collector', 'engage.monitor', 'engage.util', | 13 | packages=['engage', 'engage.collector', 'engage.media', 'engage.monitor', |
192 | 14 | 'engage.ui', 'twisted.plugins'], | 14 | 'engage.util', 'engage.ui'], |
193 | 15 | package_dir={'': 'src' }, | 15 | package_dir={'': 'src' }, |
194 | 16 | ) | 16 | ) |
195 | 17 | 17 | ||
196 | === added directory 'src/engage/media' | |||
197 | === added file 'src/engage/media/__init__.py' | |||
198 | --- src/engage/media/__init__.py 1970-01-01 00:00:00 +0000 | |||
199 | +++ src/engage/media/__init__.py 2012-09-25 15:49:24 +0000 | |||
200 | @@ -0,0 +1,91 @@ | |||
201 | 1 | # Media base class | ||
202 | 2 | # | ||
203 | 3 | # Simon C, September 2012 | ||
204 | 4 | |||
205 | 5 | # | ||
206 | 6 | # Unofficial AirPlay specification: | ||
207 | 7 | # | ||
208 | 8 | # http://nto.github.com/AirPlay.html | ||
209 | 9 | # | ||
210 | 10 | |||
211 | 11 | from zope.interface import Interface, implements | ||
212 | 12 | from twisted.internet import reactor | ||
213 | 13 | from twisted.plugin import IPlugin, getPlugins | ||
214 | 14 | from twisted.application import service | ||
215 | 15 | from twisted.python import log | ||
216 | 16 | import engage.media | ||
217 | 17 | |||
218 | 18 | class MediaSource(object): | ||
219 | 19 | """ | ||
220 | 20 | Base classes for a source of media to be displayed on a carousel | ||
221 | 21 | """ | ||
222 | 22 | |||
223 | 23 | cfg_defaults = {} | ||
224 | 24 | |||
225 | 25 | def __init__(self, name, cfg, atv, register_cb): | ||
226 | 26 | """ | ||
227 | 27 | Initialize with the configuration for this media source. Call | ||
228 | 28 | back register_cb with the number of items this media source | ||
229 | 29 | can render (and call again whenever this updates). | ||
230 | 30 | """ | ||
231 | 31 | self.name = '%s (%s)' % (name, self.__class__.__name__) | ||
232 | 32 | self.cfg = cfg.copy() | ||
233 | 33 | for k, v in self.cfg_defaults.items(): | ||
234 | 34 | if k not in self.cfg: | ||
235 | 35 | self.cfg[k] = v | ||
236 | 36 | log.msg("Starting media source %s with config %s" % (self.name, self.cfg)) | ||
237 | 37 | self.atv = atv | ||
238 | 38 | self.register_cb = register_cb | ||
239 | 39 | reactor.callLater(0, self.register_items) | ||
240 | 40 | |||
241 | 41 | def register_items(self): | ||
242 | 42 | """ | ||
243 | 43 | For documentation only in the base class. Work out how many | ||
244 | 44 | different items we could render, and tell the carousel that. | ||
245 | 45 | """ | ||
246 | 46 | |||
247 | 47 | def show(self): | ||
248 | 48 | """ | ||
249 | 49 | For documentation only in the base class. Show a random media | ||
250 | 50 | object using the provided http_agent. Return a deferred that | ||
251 | 51 | fires when done. | ||
252 | 52 | """ | ||
253 | 53 | |||
254 | 54 | @classmethod | ||
255 | 55 | def config_start(cls, cfg, ip_addr, register_cb): | ||
256 | 56 | """ | ||
257 | 57 | Start all the media sources that are configured | ||
258 | 58 | """ | ||
259 | 59 | media_sources = {} | ||
260 | 60 | for media_source in getPlugins(IMediaSource, engage.media): | ||
261 | 61 | for source in media_source.config_start(cfg, ip_addr, register_cb): | ||
262 | 62 | media_sources[source] = source.name | ||
263 | 63 | return media_sources | ||
264 | 64 | |||
265 | 65 | class IMediaSource(Interface): | ||
266 | 66 | """ | ||
267 | 67 | Twisted plugin specification for a config-started MediaSource | ||
268 | 68 | """ | ||
269 | 69 | def config_start(self, cfg, atv, register_cb): | ||
270 | 70 | """ | ||
271 | 71 | Create a MedaSource if configured | ||
272 | 72 | """ | ||
273 | 73 | |||
274 | 74 | class StartMediaSource(object): | ||
275 | 75 | """ | ||
276 | 76 | Twisted plugin for starting a MediaSource | ||
277 | 77 | """ | ||
278 | 78 | implements(IPlugin, IMediaSource) | ||
279 | 79 | |||
280 | 80 | def __init__(self, stanza_name, media_source_cls): | ||
281 | 81 | self.stanza_name = stanza_name | ||
282 | 82 | self.media_source_cls = media_source_cls | ||
283 | 83 | |||
284 | 84 | def config_start(self, cfg, atv, register_cb): | ||
285 | 85 | started = [] | ||
286 | 86 | for key in cfg: | ||
287 | 87 | if key.startswith(self.stanza_name): | ||
288 | 88 | log.msg('Starting media source %s from config stanza %s' % | ||
289 | 89 | (self.media_source_cls.__name__, key)) | ||
290 | 90 | started.append(self.media_source_cls(key, cfg[key], atv, register_cb)) | ||
291 | 91 | return started | ||
292 | 0 | 92 | ||
293 | === added file 'src/engage/media/movie.py' | |||
294 | --- src/engage/media/movie.py 1970-01-01 00:00:00 +0000 | |||
295 | +++ src/engage/media/movie.py 2012-09-25 15:49:24 +0000 | |||
296 | @@ -0,0 +1,21 @@ | |||
297 | 1 | # Static file-drive media source | ||
298 | 2 | # | ||
299 | 3 | # Simon C, September 2012 | ||
300 | 4 | |||
301 | 5 | from twisted.python import log | ||
302 | 6 | from engage.media import MediaSource, StartMediaSource | ||
303 | 7 | |||
304 | 8 | # | ||
305 | 9 | # @@@ still working out structure here, this is probably all wrong | ||
306 | 10 | # | ||
307 | 11 | |||
308 | 12 | class MovieMediaSource(MediaSource): | ||
309 | 13 | |||
310 | 14 | def register_items(self): | ||
311 | 15 | self.register_cb(self, 1) | ||
312 | 16 | |||
313 | 17 | def show(self): | ||
314 | 18 | d = self.atv.show_movie('http://172.22.22.143:8421/mp4/amy-janet.mp4') | ||
315 | 19 | return d | ||
316 | 20 | |||
317 | 21 | start = StartMediaSource('movie', MovieMediaSource) | ||
318 | 0 | 22 | ||
319 | === added file 'src/engage/media/static.py' | |||
320 | --- src/engage/media/static.py 1970-01-01 00:00:00 +0000 | |||
321 | +++ src/engage/media/static.py 2012-09-25 15:49:24 +0000 | |||
322 | @@ -0,0 +1,36 @@ | |||
323 | 1 | # Static file-drive media source | ||
324 | 2 | # | ||
325 | 3 | # Simon C, September 2012 | ||
326 | 4 | |||
327 | 5 | import os, random, re | ||
328 | 6 | from twisted.internet import reactor | ||
329 | 7 | from twisted.python import log | ||
330 | 8 | from engage.media import MediaSource, StartMediaSource | ||
331 | 9 | |||
332 | 10 | class StaticMediaSource(MediaSource): | ||
333 | 11 | |||
334 | 12 | cfg_defaults = { | ||
335 | 13 | 'refresh': '20', | ||
336 | 14 | 'weight': '1.0', | ||
337 | 15 | } | ||
338 | 16 | |||
339 | 17 | def register_items(self): | ||
340 | 18 | self.photos = [] | ||
341 | 19 | for folder, _, files in os.walk(self.cfg['photo_dir'], followlinks=True): | ||
342 | 20 | for filename in files: | ||
343 | 21 | if re.search('(?i)\.(jpg|png)', filename): | ||
344 | 22 | pathname = os.path.join(folder, filename) | ||
345 | 23 | self.photos.append(pathname) | ||
346 | 24 | log.msg("%s found %d photos to display with weight %s" % | ||
347 | 25 | (self.name, len(self.photos), self.cfg['weight'])) | ||
348 | 26 | self.register_cb(self, len(self.photos) * float(self.cfg['weight'])) | ||
349 | 27 | # re-scan every 20 minutes by default | ||
350 | 28 | reactor.callLater(60 * float(self.cfg['refresh']), self.register_items) | ||
351 | 29 | |||
352 | 30 | def show(self): | ||
353 | 31 | item = random.randint(0, len(self.photos) - 1) | ||
354 | 32 | pathname = self.photos[item] | ||
355 | 33 | d = self.atv.show_photo(pathname) | ||
356 | 34 | return d | ||
357 | 35 | |||
358 | 36 | start = StartMediaSource('static', StaticMediaSource) | ||
359 | 0 | 37 | ||
360 | === modified file 'src/engage/monitor/percent_used.py' | |||
361 | --- src/engage/monitor/percent_used.py 2012-09-18 21:21:43 +0000 | |||
362 | +++ src/engage/monitor/percent_used.py 2012-09-25 15:49:24 +0000 | |||
363 | @@ -32,9 +32,9 @@ | |||
364 | 32 | 32 | ||
365 | 33 | # Memory | 33 | # Memory |
366 | 34 | with open('/proc/meminfo') as f: | 34 | with open('/proc/meminfo') as f: |
370 | 35 | total, free, buffers = [int(f.readline().split()[1]) | 35 | total, free, buffers, cached = \ |
371 | 36 | for i in xrange(0, 3)] | 36 | [int(f.readline().split()[1]) for i in xrange(0, 4)] |
372 | 37 | mem_used = percent(total - free - buffers, total) | 37 | mem_used = percent(total - free - buffers - cached, total) |
373 | 38 | mem_buff = percent(buffers, total) | 38 | mem_buff = percent(buffers, total) |
374 | 39 | 39 | ||
375 | 40 | # Disk | 40 | # Disk |
376 | 41 | 41 | ||
377 | === modified file 'src/engage/ui/carousel.py' | |||
378 | --- src/engage/ui/carousel.py 2012-09-18 21:21:43 +0000 | |||
379 | +++ src/engage/ui/carousel.py 2012-09-25 15:49:24 +0000 | |||
380 | @@ -6,9 +6,9 @@ | |||
381 | 6 | from twisted.application import service | 6 | from twisted.application import service |
382 | 7 | from twisted.internet import reactor | 7 | from twisted.internet import reactor |
383 | 8 | from twisted.python import log | 8 | from twisted.python import log |
384 | 9 | from twisted.web.client import Agent, FileBodyProducer, HTTPConnectionPool | ||
385 | 10 | from twisted.web.http_headers import Headers | ||
386 | 11 | from engage.monitor.traffic import TrafficMonitor | 9 | from engage.monitor.traffic import TrafficMonitor |
387 | 10 | from engage.media import MediaSource | ||
388 | 11 | from engage.util.atv import ATV | ||
389 | 12 | 12 | ||
390 | 13 | class Carousel(object): | 13 | class Carousel(object): |
391 | 14 | """ | 14 | """ |
392 | @@ -36,13 +36,11 @@ | |||
393 | 36 | self.cfg = carousel_cfg | 36 | self.cfg = carousel_cfg |
394 | 37 | self.period = int(carousel_cfg.get('period', 7)) | 37 | self.period = int(carousel_cfg.get('period', 7)) |
395 | 38 | self.threshold = int(carousel_cfg.get('threshold', 10000)) | 38 | self.threshold = int(carousel_cfg.get('threshold', 10000)) |
403 | 39 | self.url = 'http://%s:7000/photo' % carousel_cfg['atv'] | 39 | self.atv = ATV(carousel_cfg['atv']) |
404 | 40 | self.agent = Agent(reactor, pool=HTTPConnectionPool(reactor)) | 40 | self.total_weight = 0 |
405 | 41 | self.headers = Headers({ | 41 | self.media_weights = {} |
406 | 42 | 'User-Agent': ['MediaControl/1.0'], | 42 | self.media_source_names = MediaSource.config_start( |
407 | 43 | 'X-Apple-Transition': ['Dissolve']}) | 43 | carousel_cfg, self.atv, self.register_media_source) |
401 | 44 | self.scan_photo_dir() | ||
402 | 45 | self.photo_idx = 0 | ||
408 | 46 | 44 | ||
409 | 47 | # Get initial SNMP stats | 45 | # Get initial SNMP stats |
410 | 48 | self.snmp_results = (0, 0, 0) # fallback for failure | 46 | self.snmp_results = (0, 0, 0) # fallback for failure |
411 | @@ -54,6 +52,14 @@ | |||
412 | 54 | # Kick off periodic photo display | 52 | # Kick off periodic photo display |
413 | 55 | reactor.callLater(self.period, self.maybe_show_next_photo) | 53 | reactor.callLater(self.period, self.maybe_show_next_photo) |
414 | 56 | 54 | ||
415 | 55 | def register_media_source(self, media_source, num_items): | ||
416 | 56 | self.media_weights[media_source] = num_items | ||
417 | 57 | self.total_weight = 0 | ||
418 | 58 | for weight in self.media_weights.values(): | ||
419 | 59 | self.total_weight += weight | ||
420 | 60 | log.msg("Registered %d out of %d items from %s" % | ||
421 | 61 | (num_items, self.total_weight, self.media_source_names[media_source])) | ||
422 | 62 | |||
423 | 57 | def maybe_show_next_photo(self): | 63 | def maybe_show_next_photo(self): |
424 | 58 | # | 64 | # |
425 | 59 | # Main workhorse of this class, that is called periodically. | 65 | # Main workhorse of this class, that is called periodically. |
426 | @@ -76,8 +82,10 @@ | |||
427 | 76 | log.msg("Carousel failure: %s" % fail) | 82 | log.msg("Carousel failure: %s" % fail) |
428 | 77 | failure.printTraceback() | 83 | failure.printTraceback() |
429 | 78 | d.addErrback(handle_failure) | 84 | d.addErrback(handle_failure) |
432 | 79 | d.addBoth(lambda r: | 85 | def reset(result): |
433 | 80 | reactor.callLater(self.period, self.maybe_show_next_photo)) | 86 | log.msg("FINAL CALLBACK!!!") |
434 | 87 | reactor.callLater(self.period, self.maybe_show_next_photo) | ||
435 | 88 | d.addBoth(reset) | ||
436 | 81 | 89 | ||
437 | 82 | def check_snmp_results(self, results): | 90 | def check_snmp_results(self, results): |
438 | 83 | # | 91 | # |
439 | @@ -93,18 +101,28 @@ | |||
440 | 93 | 101 | ||
441 | 94 | def conditionally_show_next_photo(self, tx_bps): | 102 | def conditionally_show_next_photo(self, tx_bps): |
442 | 95 | # | 103 | # |
445 | 96 | # If the tx bps is low enough, pick a photo and send it. | 104 | # If the tx bps is low enough, pick a media source at random |
446 | 97 | # Currently very dumb: round robin | 105 | # and ask it to do something. |
447 | 98 | # | 106 | # |
448 | 99 | if tx_bps >= self.threshold: | 107 | if tx_bps >= self.threshold: |
449 | 100 | log.msg("Skipping ATV photo, tx bps = %d" % tx_bps) | 108 | log.msg("Skipping ATV photo, tx bps = %d" % tx_bps) |
450 | 109 | self.atv.drop_connection() | ||
451 | 101 | else: | 110 | else: |
458 | 102 | pathname = self.photos[self.photo_idx] | 111 | r = random.randint(0, self.total_weight) |
459 | 103 | log.msg("Showing photo %s (tx bps = %d)" % (pathname, tx_bps)) | 112 | media_source = None |
460 | 104 | body = FileBodyProducer(file(pathname)) | 113 | for possible_source in self.media_source_names: |
461 | 105 | d = self.agent.request('PUT', self.url, self.headers, body) | 114 | media_source = possible_source |
462 | 106 | d.addCallbacks(self.display_done, self.display_failed) | 115 | r -= self.media_weights[possible_source] |
463 | 107 | return d | 116 | if r < 0: |
464 | 117 | break | ||
465 | 118 | if media_source is None: | ||
466 | 119 | log.msg("No media source") | ||
467 | 120 | else: | ||
468 | 121 | log.msg("Using media source %s" % (self.media_source_names[media_source])) | ||
469 | 122 | d = media_source.show() | ||
470 | 123 | d.addCallback(lambda r: "MEDIA SOURCE SHOW CALLBACK!!!") | ||
471 | 124 | d.addErrback(self.display_failed) | ||
472 | 125 | return d | ||
473 | 108 | 126 | ||
474 | 109 | def get_second_snmp_results(self, _): | 127 | def get_second_snmp_results(self, _): |
475 | 110 | # | 128 | # |
476 | @@ -125,35 +143,6 @@ | |||
477 | 125 | log.msg("Display failed: %s" % failure) | 143 | log.msg("Display failed: %s" % failure) |
478 | 126 | failure.printTraceback() | 144 | failure.printTraceback() |
479 | 127 | 145 | ||
480 | 128 | def display_done(self, response): | ||
481 | 129 | # | ||
482 | 130 | # Something went wrong sending a photo to the ATV - log it | ||
483 | 131 | # | ||
484 | 132 | if response.code != 200: | ||
485 | 133 | log.msg("Bad display response: key %s, code %s, phrase %s" % | ||
486 | 134 | (response.version, response.code, response.phrase)) | ||
487 | 135 | |||
488 | 136 | # Either way, pick a new photo at random | ||
489 | 137 | new_photo_idx = self.photo_idx | ||
490 | 138 | while new_photo_idx == self.photo_idx: | ||
491 | 139 | new_photo_idx = random.randint(0, len(self.photos) - 1) | ||
492 | 140 | self.photo_idx = new_photo_idx | ||
493 | 141 | |||
494 | 142 | def scan_photo_dir(self): | ||
495 | 143 | # | ||
496 | 144 | # Scan the configured path for any plausible photos. Re-scan | ||
497 | 145 | # sometimes in case new photos have arrived. | ||
498 | 146 | # | ||
499 | 147 | self.photos = [] | ||
500 | 148 | for folder, _, files in os.walk(self.cfg['photo_dir']): | ||
501 | 149 | for filename in files: | ||
502 | 150 | if re.search('(?i)\.jpg', filename): | ||
503 | 151 | pathname = os.path.join(folder, filename) | ||
504 | 152 | self.photos.append(pathname) | ||
505 | 153 | log.msg("Found %d photos to display after scan" % len(self.photos)) | ||
506 | 154 | # re-scan every 20 minutes | ||
507 | 155 | reactor.callLater(60 * 20, self.scan_photo_dir) | ||
508 | 156 | |||
509 | 157 | class CarouselService(service.Service): | 146 | class CarouselService(service.Service): |
510 | 158 | """ | 147 | """ |
511 | 159 | Twisted Service encapsulating one carousel | 148 | Twisted Service encapsulating one carousel |
512 | 160 | 149 | ||
513 | === modified file 'src/engage/ui/web.py' | |||
514 | --- src/engage/ui/web.py 2012-09-18 21:21:43 +0000 | |||
515 | +++ src/engage/ui/web.py 2012-09-25 15:49:24 +0000 | |||
516 | @@ -159,6 +159,8 @@ | |||
517 | 159 | site.putChild("", Main(config)) | 159 | site.putChild("", Main(config)) |
518 | 160 | site.putChild("graph", Graph(config)) | 160 | site.putChild("graph", Graph(config)) |
519 | 161 | site.putChild("png", File('%s/png' % web_config['web_static_dir'])) | 161 | site.putChild("png", File('%s/png' % web_config['web_static_dir'])) |
520 | 162 | site.putChild("mp4", File('%s/mp4' % web_config['web_static_dir'])) | ||
521 | 163 | |||
522 | 162 | 164 | ||
523 | 163 | web = Site(site) | 165 | web = Site(site) |
524 | 164 | tcp_service = internet.TCPServer(int(web_config['http_port']), web) | 166 | tcp_service = internet.TCPServer(int(web_config['http_port']), web) |
525 | 165 | 167 | ||
526 | === added file 'src/engage/util/atv.py' | |||
527 | --- src/engage/util/atv.py 1970-01-01 00:00:00 +0000 | |||
528 | +++ src/engage/util/atv.py 2012-09-25 15:49:24 +0000 | |||
529 | @@ -0,0 +1,94 @@ | |||
530 | 1 | # Apple TV control | ||
531 | 2 | # | ||
532 | 3 | # Simon C, September 2012 | ||
533 | 4 | |||
534 | 5 | import re | ||
535 | 6 | from StringIO import StringIO | ||
536 | 7 | from twisted.internet import defer, reactor | ||
537 | 8 | from twisted.python import log | ||
538 | 9 | from twisted.internet.protocol import Protocol | ||
539 | 10 | from twisted.web.client import Agent, HTTPConnectionPool | ||
540 | 11 | from twisted.web.client import FileBodyProducer | ||
541 | 12 | from twisted.web.http_headers import Headers | ||
542 | 13 | |||
543 | 14 | POLL_INTERVAL = 1 | ||
544 | 15 | |||
545 | 16 | class ResponseReceiver(Protocol): | ||
546 | 17 | |||
547 | 18 | def __init__(self, finished, fire_again): | ||
548 | 19 | self.finished = finished | ||
549 | 20 | self.fire_again = fire_again | ||
550 | 21 | self.remaining = POLL_INTERVAL | ||
551 | 22 | |||
552 | 23 | def dataReceived(self, data): | ||
553 | 24 | position, duration = [float(re.search(key + ': ([\d.]+)', data).group(1)) | ||
554 | 25 | for key in ('position', 'duration')] | ||
555 | 26 | if duration > 0: | ||
556 | 27 | remaining = duration - position | ||
557 | 28 | if remaining < self.remaining: | ||
558 | 29 | self.remaining = remaining | ||
559 | 30 | |||
560 | 31 | log.msg("Video at %f/%f; timer reset to %f" % | ||
561 | 32 | (position, duration, self.remaining)) | ||
562 | 33 | |||
563 | 34 | def connectionLost(self, reason): | ||
564 | 35 | if self.remaining < POLL_INTERVAL: | ||
565 | 36 | log.msg("Last %f seconds, just waiting" % self.remaining) | ||
566 | 37 | else: | ||
567 | 38 | self.finished.addCallback(self.fire_again) | ||
568 | 39 | reactor.callLater(self.remaining, self.finished.callback, None) | ||
569 | 40 | |||
570 | 41 | |||
571 | 42 | class ATV(object): | ||
572 | 43 | |||
573 | 44 | HEADERS = Headers({'User-Agent': ['MediaControl/1.0'], | ||
574 | 45 | 'X-Apple-Transition': ['Dissolve']}) | ||
575 | 46 | |||
576 | 47 | def __init__(self, ip_addr): | ||
577 | 48 | self.http_agent = None | ||
578 | 49 | self.base_url = 'http://%s:7000' % ip_addr | ||
579 | 50 | |||
580 | 51 | def drop_connection(self): | ||
581 | 52 | self.http_agent = None | ||
582 | 53 | |||
583 | 54 | def _check_connection(self): | ||
584 | 55 | if self.http_agent is None: | ||
585 | 56 | self.http_agent = Agent(reactor, pool=HTTPConnectionPool(reactor)) | ||
586 | 57 | |||
587 | 58 | def show_photo(self, pathname): | ||
588 | 59 | self._check_connection() | ||
589 | 60 | body = FileBodyProducer(file(pathname)) | ||
590 | 61 | d = self.http_agent.request('PUT', self.base_url + '/photo', | ||
591 | 62 | self.HEADERS, body) | ||
592 | 63 | def display_done(response): | ||
593 | 64 | if response.code != 200: | ||
594 | 65 | log.msg("Bad response from photo %s: key %s, code %s, phrase %s" % | ||
595 | 66 | (pathname, response.version, response.code, response.phrase)) | ||
596 | 67 | d.addCallback(display_done) | ||
597 | 68 | return d | ||
598 | 69 | |||
599 | 70 | def show_movie(self, url): | ||
600 | 71 | self._check_connection() | ||
601 | 72 | body_string = 'Content-Location: %s\nStart-Position: 0\n' % url | ||
602 | 73 | body = FileBodyProducer(StringIO(body_string)) | ||
603 | 74 | url = self.base_url + '/play' | ||
604 | 75 | d = self.http_agent.request('POST', url, self.HEADERS, body) | ||
605 | 76 | d.addCallback(self.get_scrub_soon) | ||
606 | 77 | return d | ||
607 | 78 | |||
608 | 79 | def get_scrub_soon(self, result): | ||
609 | 80 | d = defer.Deferred() | ||
610 | 81 | d.addCallback(self.get_scrub) | ||
611 | 82 | reactor.callLater(POLL_INTERVAL, lambda: d.callback(result)) | ||
612 | 83 | return d | ||
613 | 84 | |||
614 | 85 | def get_scrub(self, result): | ||
615 | 86 | url = self.base_url + '/scrub' | ||
616 | 87 | d = self.http_agent.request('GET', url) | ||
617 | 88 | d.addCallback(self.display_scrub) | ||
618 | 89 | return d | ||
619 | 90 | |||
620 | 91 | def display_scrub(self, response): | ||
621 | 92 | d = defer.Deferred() | ||
622 | 93 | response.deliverBody(ResponseReceiver(d, self.get_scrub)) | ||
623 | 94 | return d | ||
624 | 0 | 95 | ||
625 | === added file 'src/engage/util/twisted_plugin.py' | |||
626 | --- src/engage/util/twisted_plugin.py 1970-01-01 00:00:00 +0000 | |||
627 | +++ src/engage/util/twisted_plugin.py 2012-09-25 15:49:24 +0000 | |||
628 | @@ -0,0 +1,65 @@ | |||
629 | 1 | # EnGage twisted plugin | ||
630 | 2 | # | ||
631 | 3 | # Simon C, September 2012 | ||
632 | 4 | |||
633 | 5 | from zope.interface import implements | ||
634 | 6 | from twisted.python import usage | ||
635 | 7 | from twisted.plugin import IPlugin | ||
636 | 8 | from twisted.application import service | ||
637 | 9 | |||
638 | 10 | from engage.collector.rrd import RRDCollector | ||
639 | 11 | from engage.ui import web, carousel | ||
640 | 12 | from engage.util import config | ||
641 | 13 | |||
642 | 14 | class Options(usage.Options): | ||
643 | 15 | |||
644 | 16 | optParameters = [ | ||
645 | 17 | ('conf', 'c', '/etc/engage.conf', 'EnGage configuration file.'), | ||
646 | 18 | ] | ||
647 | 19 | |||
648 | 20 | # | ||
649 | 21 | # Built-in configuration. We don't really want the admin messing with the first | ||
650 | 22 | # two; the others are more just handy defaults. | ||
651 | 23 | # | ||
652 | 24 | collector_cfg = { | ||
653 | 25 | 'interval': 10, | ||
654 | 26 | 'rrd_dir': '/var/lib/engage/rrd', | ||
655 | 27 | } | ||
656 | 28 | |||
657 | 29 | web_cfg = { | ||
658 | 30 | 'web_static_dir': '/usr/share/engage/static', | ||
659 | 31 | 'http_prefix': '', | ||
660 | 32 | 'http_port': '8421', | ||
661 | 33 | 'title': 'EnGage', | ||
662 | 34 | } | ||
663 | 35 | |||
664 | 36 | class EngageServiceMaker(object): | ||
665 | 37 | |||
666 | 38 | implements(service.IServiceMaker, IPlugin) | ||
667 | 39 | |||
668 | 40 | tapname = "engage" | ||
669 | 41 | description = "EnGage system/network monitoring service." | ||
670 | 42 | options = Options | ||
671 | 43 | |||
672 | 44 | def makeService(self, options): | ||
673 | 45 | top_service = service.MultiService() | ||
674 | 46 | |||
675 | 47 | # Merge the user and built-in configuration | ||
676 | 48 | all_config = config.parse(options['conf']) | ||
677 | 49 | for k, v in all_config['Main'].items(): | ||
678 | 50 | web_cfg[k] = v | ||
679 | 51 | collector_cfg[k] = v | ||
680 | 52 | |||
681 | 53 | # Start monitors based on configuration, with data collected by RRD | ||
682 | 54 | RRDCollector.config_start_monitors(collector_cfg, | ||
683 | 55 | all_config, | ||
684 | 56 | top_service) | ||
685 | 57 | |||
686 | 58 | # Start carousel if configured | ||
687 | 59 | if 'Carousel' in all_config: | ||
688 | 60 | carousel.start(all_config, top_service) | ||
689 | 61 | |||
690 | 62 | # Start the web server too | ||
691 | 63 | web.start(web_cfg, top_service) | ||
692 | 64 | |||
693 | 65 | return top_service | ||
694 | 0 | 66 | ||
695 | === modified file 'src/twisted/plugins/engage_plugin.py' | |||
696 | --- src/twisted/plugins/engage_plugin.py 2012-09-18 21:21:43 +0000 | |||
697 | +++ src/twisted/plugins/engage_plugin.py 2012-09-25 15:49:24 +0000 | |||
698 | @@ -1,67 +1,6 @@ | |||
765 | 1 | # EnGage twisted plugin | 1 | # |
766 | 2 | # | 2 | # Crazy file to allow embedding of Twisted as an egg, for stock Ubuntu |
767 | 3 | # Simon C, September 2012 | 3 | # distributions with a pre-12.0.0 version |
768 | 4 | 4 | # | |
769 | 5 | from zope.interface import implements | 5 | from engage.util.twisted_plugin import EngageServiceMaker |
704 | 6 | from twisted.python import usage | ||
705 | 7 | from twisted.plugin import IPlugin | ||
706 | 8 | from twisted.application import service | ||
707 | 9 | |||
708 | 10 | from engage.collector.rrd import RRDCollector | ||
709 | 11 | from engage.ui import web, carousel | ||
710 | 12 | from engage.util import config | ||
711 | 13 | |||
712 | 14 | class Options(usage.Options): | ||
713 | 15 | |||
714 | 16 | optParameters = [ | ||
715 | 17 | ('conf', 'c', '/etc/engage.conf', 'EnGage configuration file.'), | ||
716 | 18 | ] | ||
717 | 19 | |||
718 | 20 | # | ||
719 | 21 | # Built-in configuration. We don't really want the admin messing with the first | ||
720 | 22 | # two; the others are more just handy defaults. | ||
721 | 23 | # | ||
722 | 24 | collector_cfg = { | ||
723 | 25 | 'interval': 10, | ||
724 | 26 | 'rrd_dir': '/var/lib/engage/rrd', | ||
725 | 27 | } | ||
726 | 28 | |||
727 | 29 | web_cfg = { | ||
728 | 30 | 'web_static_dir': '/usr/share/engage/static', | ||
729 | 31 | 'http_prefix': '', | ||
730 | 32 | 'http_port': '8421', | ||
731 | 33 | 'title': 'EnGage', | ||
732 | 34 | } | ||
733 | 35 | |||
734 | 36 | class EngageServiceMaker(object): | ||
735 | 37 | |||
736 | 38 | implements(service.IServiceMaker, IPlugin) | ||
737 | 39 | |||
738 | 40 | tapname = "engage" | ||
739 | 41 | description = "EnGage system/network monitoring service." | ||
740 | 42 | options = Options | ||
741 | 43 | |||
742 | 44 | def makeService(self, options): | ||
743 | 45 | top_service = service.MultiService() | ||
744 | 46 | |||
745 | 47 | # Merge the user and built-in configuration | ||
746 | 48 | all_config = config.parse(options['conf']) | ||
747 | 49 | for k, v in all_config['Main'].items(): | ||
748 | 50 | web_cfg[k] = v | ||
749 | 51 | collector_cfg[k] = v | ||
750 | 52 | |||
751 | 53 | # Start monitors based on configuration, with data collected by RRD | ||
752 | 54 | RRDCollector.config_start_monitors(collector_cfg, | ||
753 | 55 | all_config, | ||
754 | 56 | top_service) | ||
755 | 57 | |||
756 | 58 | # Start carousel if configured | ||
757 | 59 | if 'Carousel' in all_config: | ||
758 | 60 | carousel.start(all_config, top_service) | ||
759 | 61 | |||
760 | 62 | # Start the web server too | ||
761 | 63 | web.start(web_cfg, top_service) | ||
762 | 64 | |||
763 | 65 | return top_service | ||
764 | 66 | |||
770 | 67 | service_maker = EngageServiceMaker() | 6 | service_maker = EngageServiceMaker() |
771 | 68 | 7 | ||
772 | === removed file 'start' | |||
773 | --- start 2012-09-14 12:24:16 +0000 | |||
774 | +++ start 1970-01-01 00:00:00 +0000 | |||
775 | @@ -1,19 +0,0 @@ | |||
776 | 1 | hostname=$(hostname) | ||
777 | 2 | if [ z$ENGAGE == z ]; then | ||
778 | 3 | engage=engage | ||
779 | 4 | instance=$hostname | ||
780 | 5 | else | ||
781 | 6 | engage=test_engage | ||
782 | 7 | instance=test_$hostname | ||
783 | 8 | fi | ||
784 | 9 | CFGFILE=$PWD/etc/$instance.conf | ||
785 | 10 | PIDFILE=$PWD/var/run/$engage.pid | ||
786 | 11 | LOGFILE=$PWD/var/log/$instance.log | ||
787 | 12 | |||
788 | 13 | if [ ! -f $CFGFILE ]; then | ||
789 | 14 | echo No specific config file for host $hostname, using default | ||
790 | 15 | CFGFILE=$PWD/etc/engage.conf | ||
791 | 16 | fi | ||
792 | 17 | |||
793 | 18 | cd src | ||
794 | 19 | twistd $* --pidfile $PIDFILE --logfile $LOGFILE engage -c $CFGFILE | ||
795 | 20 | 0 | ||
796 | === removed file 'stop' | |||
797 | --- stop 2012-09-14 12:24:16 +0000 | |||
798 | +++ stop 1970-01-01 00:00:00 +0000 | |||
799 | @@ -1,17 +0,0 @@ | |||
800 | 1 | #!/bin/bash | ||
801 | 2 | |||
802 | 3 | if [ z$ENGAGE == z ]; then | ||
803 | 4 | engage=engage | ||
804 | 5 | rrd_dir=var/rrd | ||
805 | 6 | else | ||
806 | 7 | engage=test_engage | ||
807 | 8 | rrd_dir=var/test-rrd | ||
808 | 9 | fi | ||
809 | 10 | PIDFILE=$PWD/var/run/$engage.pid | ||
810 | 11 | |||
811 | 12 | if [ -f $PIDFILE ]; then | ||
812 | 13 | cd $rrd_dir | ||
813 | 14 | # pause for most recent data collection, to avoid holes in data set | ||
814 | 15 | perl -e '$| = 1; while (time - (stat((glob("*.rrd"))[0]))[9] != 1) { print "."; sleep 1; }; print "\n"' | ||
815 | 16 | kill $(cat $PIDFILE) | ||
816 | 17 | fi |