1
0
Fork 0
mirror of https://github.com/iiab/iiab.git synced 2025-03-09 15:40:17 +00:00

Move 19 roles into roles/0-DEPRECATED-ROLES

This commit is contained in:
root 2020-01-24 02:27:21 -05:00
parent 0e39c42bbd
commit 2218d2334b
124 changed files with 5 additions and 1 deletions

View file

@ -0,0 +1,139 @@
Serve XO activities from the XS school server
=============================================
XO laptops can update their activities via HTTP, using specially
microformatted html pages to determine what to download.
This package imports XO activities from a USB stick and generates the
correct html to serve them, in as many languages as it knows how,
using the localisation information from the activity bundles. Content
negotiation between Apache and the laptops decides what html is seen.
The URL for this index is http://schoolserver/activities/.
A facility exists to add extra descriptive html to the generated
indexes.
USB import
----------
When a USB drive is inserted, the server looks for a directory in the
root directory called xs-activity-server. In there it expects to find
any number of activity bundles, a file called manifest.sha1, and
optionally a file or files with the suffix ".info". Depending on the
configuration of the school server, a file called "manifest.sha1.sig"
might also be required.
Activity bundles are zip files with the suffix .xo and an internal
layout described at http://wiki.laptop.org/go/Activity_bundles.
The manifest file should contain sha1sums for each activity bundle and
the metadata files, as if you had run
sha1sum *.xo *.info > manifest.sha1
in the directory.
If full XS security is enabled (by the presence of /etc/xs-security-on
-- see the xs-tools documentation), then manifest.sha1.sig should
contain a detached GPG signature for manifest.sha1, signed by a key
that the XS knows. If the school server lacks the /etc/xs-security-on
flag, the manifest.sha1.sig file is ignored.
Multiple languages
------------------
Activities can contain localisation information, which usually
consists of a translated activity name. The localised information is
found in the bundle in a directory like:
SomeWonderful.activity/locale/pt-BR/activity.linfo
where pt-BR is an RFC1788 language code. If any activity contains an
activity.linfo file for a language, then an index is generated. The
server has templates for indexes in some languages (currently Spanish
and English); for other languages the indexes will be in English
except for the localised names.
These index files are saved with names like
activities/index.html.zh-es. You can choose to look at them directly
that way, or let your browser decide which one is best for you by
visiting activities/index.html.
If some activities lack localised information for a multi-part
language code, the index will include information that exists for the
corresponding single part code, before defaulting to English. For
example, a zh-CN page will include zh localisation if need be. (This
may not always be the best result: bn and bn-IN appear to use
different scripts).
Including extra descriptions
----------------------------
The optional .info files in the xs-activation-server directory should
consist of sections in this format:
[com.microsoft.Word]
description = Write replacement, without distraction of collaboration.
[some.other.Something]
description = another description, all on one line.
If a section heading (in square brackets) matches the bundle_id or
service_name of an activity, the description will be displayed on the
generated html page. This information is not used by automatic
updates, but it might assist children in browsing and manually
installing activities. Note: there is no clever localisation here.
Multiple versions
-----------------
Over the course of a server deployment, an activity might be updated
several times. To preserve disk space, only the 4 most recent
versions of an activity are kept. Links to the second, third and
fourth newest versions are presented in the activities html file, but
these do not use the activity microformat and will not be visible to
automated updaters.
To determine which activities are the most recent, the file's modification
times (mtime) are examined. The version number is not considered here.
Note: If you plug in a USB stick with very out-of-date activities they
will be deleted as soon as they get on the server.
HTML microformat
----------------
The microformat is described at
http://wiki.laptop.org/go/Activity_microformat.
Utility script
--------------
/usr/bin/xs-check-activities will print statistics about a directory
of activities to stderr. Its output is not particularly well
formatted or explained, but it is there if you want it.
Files and directories
---------------------
Activities are stored in /library/xs-activity-server/activities, with
the html index being index.html in that directory. Apache is coaxed
into serving this by /etc/httpd/conf.d/xs-activity-server.conf.
Bugs
----
Old versions are only saved if the different versions have different
file names. Most activity bundles have names like 'Maze-4.xo',
'Maze-5.xo' and so on, but some lack the version number in the file
name, so the most recently imported version ends up overwriting the
older ones.
Source
------
This role is based on the xs-activity-server rpm.
http://dev.laptop.org/git/users/martin/xs-activity-server/ v0.4 release

View file

@ -0,0 +1,3 @@
activity_server_enabled: False
activity_server_install: True

View file

@ -0,0 +1,30 @@
#!/usr/bin/python
# Copyright (C) 2008 One Laptop Per Child Association, Inc.
# Licensed under the terms of the GNU GPL v2 or later; see COPYING for details.
#
# written by Douglas Bagnall <douglas@paradise.net.nz>
"""This script reads activity.info from bundle files and reports on
their quality.
"""
import xs_activities
import sys, os
xs_activities.USE_STDERR = True
show_all = '--show-all' in sys.argv
if show_all:
sys.argv.remove('--show-all')
try:
directory = sys.argv[1]
os.stat(directory)
except (IndexError, OSError):
print __doc__
print "USAGE: %s DIRECTORY" % sys.argv[0]
sys.exit(1)
xs_activities.check_all_bundles(directory, show_all)

View file

@ -0,0 +1,93 @@
#!/usr/bin/python
# Copyright (C) 2008 One Laptop Per Child Association, Inc.
# Licensed under the terms of the GNU GPL v2 or later; see COPYING for details.
#
# written by Douglas Bagnall <douglas@paradise.net.nz>
"""Read activity.info files from a directory full of activity bundles
and write an appropriate html manifest of the most recent versions.
The manifest uses the OLPC activity microformat:
http://wiki.laptop.org/go/Activity_microformat
This is put in a place where apache can find it, and apache will serve
it and the activities to the laptops. Messages go to /var/log/user.log.
"""
import os
import sys
import shutil
from time import time
import xs_activities
INPUT_DIR = "/library/xs-activity-server/activities"
OUTPUT_LINK = "/library/xs-activity-server/www"
try:
CURRENT_DIR = os.readlink(OUTPUT_LINK)
except OSError:
CURRENT_DIR = None
def create_dir_manifest(dir_path):
manifest = []
os.chdir(dir_path)
for root, dirs, files in os.walk("."):
for filename in files:
if not filename.endswith(".xo") and not filename.endswith(".xol"):
continue
path = os.path.join(root, filename)
s = os.stat(path)
manifest.append((path, s.st_ino))
manifest.sort()
return manifest
def input_changed():
if CURRENT_DIR is None:
return True
input_manifest = create_dir_manifest(INPUT_DIR)
current_manifest = create_dir_manifest(CURRENT_DIR)
return input_manifest != current_manifest
if not input_changed():
# no changes or nothing to do
sys.exit(0)
# create new output dir
OUTPUT_DIR = OUTPUT_LINK + "." + str(time())
os.mkdir(OUTPUT_DIR)
# link in all activities
os.chdir(INPUT_DIR)
for root, dirs, files in os.walk("."):
output_dir = os.path.join(OUTPUT_DIR, root)
if not os.path.isdir(output_dir):
os.makedirs(output_dir)
for filename in files:
if not filename.endswith(".xo") and not filename.endswith(".xol"):
continue
path = os.path.join(root, filename)
output_path = os.path.join(output_dir, filename)
os.link(path, output_path)
# create html index
output_html = os.path.join(output_dir, 'index.html')
xs_activities.htmlise_bundles(output_dir, output_html)
# update symlink atomically
link = OUTPUT_DIR + ".lnk"
os.symlink(OUTPUT_DIR, link)
os.rename(link, OUTPUT_LINK)
# remove old index
if CURRENT_DIR is not None:
shutil.rmtree(CURRENT_DIR, ignore_errors=True)

View file

@ -0,0 +1,10 @@
<div class="olpc-activity-info">
<h2>%(name)s</h2>
%(description)s
<ul>
<li>Identifier: <span class="olpc-activity-id">%(bundle_id)s</span></li>
<li>Version: <span class="olpc-activity-version">%(activity_version)s</span></li>
<li>URL: <span class="olpc-activity-url"><a href="%(bundle_url)s">%(bundle_url)s</a></span></li>
<li style="display: %(show_older_versions)s">Older versions: %(older_versions)s</li>
</ul>
</div>

View file

@ -0,0 +1,7 @@
<html>
<body>
<h1 id="olpc-activity-group-name">Locally available activities</h1>
<p id="olpc-activity-group-desc">These activities are stored on the school server.</p>
%(activities)s
</body>
</html>

View file

@ -0,0 +1,10 @@
<div class="olpc-activity-info">
<h2>%(name)s</h2>
%(description)s
<ul>
<li>Identificador: <span class="olpc-activity-id">%(bundle_id)s</span></li>
<li>Versión: <span class="olpc-activity-version">%(activity_version)s</span></li>
<li>URL: <span class="olpc-activity-url"><a href="%(bundle_url)s">%(bundle_url)s</a></span></li>
<li style="display: %(show_older_versions)s">Versiones anteriores: %(older_versions)s</li>
</ul>
</div>

View file

@ -0,0 +1,8 @@
<!DOCTYPE html>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<body>
<h1 id="olpc-activity-group-name">Actividades disponibles localmente</h1>
<p id="olpc-activity-group-desc">Estas actividades están almacenads en el servidor de la escuela.</p>
%(activities)s
</body>
</html>

View file

@ -0,0 +1,10 @@
<div class="olpc-activity-info">
<h2>%(name)s</h2>
%(description)s
<ul>
<li>Identifiant: <span class="olpc-activity-id">%(bundle_id)s</span></li>
<li>Version: <span class="olpc-activity-version">%(activity_version)s</span></li>
<li>URL: <span class="olpc-activity-url"><a href="%(bundle_url)s">%(bundle_url)s</a></span></li>
<li style="display: %(show_older_versions)s">Anciennes versions: %(older_versions)s</li>
</ul>
</div>

View file

@ -0,0 +1,8 @@
<!DOCTYPE html>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<body>
<h1 id="olpc-activity-group-name">Activités disponibles localement</h1>
<p id="olpc-activity-group-desc">Ces activités sont stockées sur le serveur de lécole.</p>
%(activities)s
</body>
</html>

View file

@ -0,0 +1,10 @@
<div class="olpc-activity-info">
<h2>%(name)s</h2>
%(description)s
<ul>
<li>Pou idantifye: <span class="olpc-activity-id">%(bundle_id)s</span></li>
<li>Vesyon: <span class="olpc-activity-version">%(activity_version)s</span></li>
<li>URL: <span class="olpc-activity-url"><a href="%(bundle_url)s">%(bundle_url)s</a></span></li>
<li style="display: %(show_older_versions)s">Vesyon ansyen: %(older_versions)s</li>
</ul>
</div>

View file

@ -0,0 +1,8 @@
<!DOCTYPE html>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<body>
<h1 id="olpc-activity-group-name">Aktivite ki disponib nan Bwat la</h1>
<p id="olpc-activity-group-desc">Aktivite sa yo disponib sou sit lekòl la.</p>
%(activities)s
</body>
</html>

View file

@ -0,0 +1,10 @@
<html>
<body>
<h1 id="olpc-activity-group-name">Locally available activities</h1>
<p id="olpc-activity-group-desc">These activities are stored on the school server.</p>
<div class="olpc-activity-info">
There are currently no activities. Insert a USB drive with activities on it to add some.
</div>
</body>
</html>

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<body>
<h1 id="olpc-activity-group-name">Activités disponibles localement</h1>
<p id="olpc-activity-group-desc">Ces activités sont stockées sur le serveur de lécole.</p>
<div class="olpc-activity-info">
Il n'y a pas d'activités. Insérez une clé USB avec des activités sur pour ajouter.
</div>
</body>
</html>

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<body>
<h1 id="olpc-activity-group-name">Aktivite ki disponib nan Bwat la</h1>
<p id="olpc-activity-group-desc">Aktivite sa yo disponib sou sit lekòl la.</p>
<div class="olpc-activity-info">
Gen kounye a pa gen okenn aktivite. Antre yon USB ak aktivite sou li ajoute kèk.
</div>
</body>
</html>

View file

@ -0,0 +1,535 @@
# Copyright (C) 2008 One Laptop Per Child Association, Inc.
# Licensed under the terms of the GNU GPL v2 or later; see COPYING for details.
#
# written by Douglas Bagnall <douglas@paradise.net.nz>
"""Functions for processing XO activities, either for indexing and
presentaton to the laptops, or for diagnostics.
"""
import os, sys, shutil
import zipfile
import re
from cStringIO import StringIO
#import traceback
import syslog
from ConfigParser import SafeConfigParser
# we no longer really have a default in that it is set in the conf file
# we assume that we have a lang_template for the default language
TEMPLATE_DIR = '/library/xs-activity-server/lang_templates'
DEFAULT_LANG = 'en'
# how many versions before the latest are worth having around.
KEEP_OLD_VERSIONS = 3
#print to stderr, rathe than syslog?
USE_STDERR=False
REQUIRED_TAGS = ('bundle_id', 'activity_version', 'host_version', 'name', 'license')
OPTIONAL_TAGS = ('show_launcher', 'exec', 'mime_types', 'icon')
#XXX need either icon or show_launcher=no
def log(msg, level=syslog.LOG_NOTICE):
if USE_STDERR:
print >> sys.stderr, msg
else:
syslog.openlog( 'xs-activity-server', 0, syslog.LOG_USER )
syslog.syslog(level, msg)
syslog.closelog()
class BundleError(Exception):
pass
class Bundle(object):
def __init__(self, bundle):
self.linfo = {}
self.zf = zipfile.ZipFile(bundle)
# The activity path will be 'Something.activity/activity/activity.info'
for p in self.zf.namelist():
if p.endswith(self.INFO_PATH):
self.raw_data = read_info_file(self.zf, p, self.INFO_SECTION)
# the file name itself is needed for the URL
self.url = os.path.basename(bundle)
self.mtime = os.stat(bundle).st_mtime
self.name = self.raw_data.get('name')
self.license = self.raw_data.get('license', None)
# child ctor should now call
# _set_bundle_id
# _set_version
# _set_description
def _set_bundle_id(self, id):
if id is None:
raise BundleError("bad bundle: No bundle ID")
self.bundle_id = id
if self.name is None:
self.name = id
def _set_version(self, version):
self.version = version
def _set_description(self, description):
self.description = description
def __cmp__(self, other):
"""Alphabetical sort (locale dependant of course)"""
if self.bundle_id == other.bundle_id:
return cmp(self.version, other.version)
return cmp(self.name, other.name)
def set_older_versions(self, versions):
"""Versions should be a list of (version number, version tuples)"""
self.older_versions = ', '.join('<a href="%s">%s</a>' % (v.url, v.version) for v in versions)
def to_html(self, locale, template=None):
"""Fill in the template with data approriate for the locale."""
if template is None:
template = read_template('activity', locale)
d = {'older_versions': self.older_versions,
'bundle_id': self.bundle_id,
'activity_version': self.version,
'bundle_url': self.url,
'name': self.name,
'description': self.description,
}
d.update(self.linfo.get(locale, {}))
if d['older_versions']:
d['show_older_versions'] = 'inline'
else:
d['show_older_versions'] = 'none'
return template % d
def get_name(self, locale=None):
return self.name
class Content(Bundle):
INFO_PATH = "library/library.info"
INFO_SECTION = "Library"
def __init__(self, bundle):
super(Content, self).__init__(bundle)
d = self.raw_data
# bundle_id is often missing; service name is used instead.
self._set_bundle_id(d.get('global_name', None))
self._set_version(d.get('library_version', 1))
self._set_description(d.get('long_name', ''))
def debug(self, force_recheck=False):
# FIXME: implement debug checking for content bundles
return {}
class Activity(Bundle):
INFO_PATH = "activity/activity.info"
INFO_SECTION = "Activity"
#Activities appear to be looser than RFC3066, using e.g. _ in place of -.
linfo_re = re.compile(r'/locale/([A-Za-z]+[\w-]*)/activity.linfo$')
def __init__(self, bundle):
"""Takes a zipped .xo bundle name, returns a dictionary of its
activity info. Can raise a variety of exceptions, all of
which should indicate the bundle is invalid."""
super(Activity, self).__init__(bundle)
# The locale info will be Something.activity/locale/xx_XX/activity.linfo
for p in self.zf.namelist():
linfo = self.linfo_re.search(p)
if linfo:
lang = canonicalise(linfo.group(1))
self.linfo[lang] = read_info_file(self.zf, p, self.INFO_SECTION)
# Unfortunately the dictionary lacks some information, and
# stores other bits in inconsistent ways.
d = self.raw_data
# bundle_id is often missing; service name is used instead.
self._set_bundle_id(d.get('bundle_id', d.get('service_name')))
self._set_version(d.get('activity_version', 1))
self._set_description(d.get('description', ''))
def debug(self, force_recheck=False):
"""Make a copy of the raw data with added bits so we can work
out what is going on. This is useful for diagnosing problems
with odd activities and composing tut-tut-ing emails to their
authors.
Not used in production."""
if hasattr(self, '_debug_data') and not force_recheck:
return self._debug_data
d = self.raw_data.copy()
correct_forms = {
'name': str.upper,
'activity_version': str.isdigit,
'host_version': str.isdigit,
'bundle_id': re.compile(r'^[\w.]+$').match,
'service_name': re.compile(r'^[\w.]+$').match,
'icon': re.compile(r'^[\S]+$').match,
'exec': str.upper,
'mime_types': re.compile(r'^([\w.+-]+/[\w.+-]+;?)*$').match,
'update_url': re.compile(r'^http://([\w-]+\.?)+(:\d+)?(/[\w~%.-]+)*$').match,
#'update_url': re.compile(r'^$').match,
'show_launcher': re.compile(r'^(yes)|(no)$').match,
'class': re.compile(r'^(\w+.?)+$').match,
'license': str.upper,
#'license': re.compile(r'^GPLv[23]\+?$').match,
}
for k, v in d.items():
if k in correct_forms:
f = correct_forms.get(k, len)
if not f(v):
d['BAD ' + k] = v
rcount = 0
for k in REQUIRED_TAGS:
if k not in d:
d['LACKS %s' % k] = True
rcount += 1
d['MISSING KEYS'] = rcount
for t in OPTIONAL_TAGS:
if t not in d:
d['NO ' + t] = True
if not 'icon' in d and d.get('show_launcher') != 'no':
d['NO icon AND show_launcher'] = True
self._debug_data = d
return d
def get_name(self, locale):
"""Return the best guess at a name for the locale."""
for loc in locale_search_path(locale):
if loc in self.linfo and 'name' in self.linfo[loc]:
return self.linfo[loc]['name']
return super(Activity, self).get_name()
def check_all_bundles(directory, show_all_bundles=False):
"""A verbose debug function."""
all_bundles = []
unique_bundles = {}
counts = {}
# watch for these tags and print out the lists
bad_contents = {}
all_linfo = {}
unique_linfo = {}
linfo_keys = {}
log('Checking all activities in %s\n' % directory)
for f in os.listdir(directory):
if not f.endswith('.xo') and not f.endswith('.xol'):
continue
#log(f)
try:
if f.endswith('.xo'):
bundle = Activity(os.path.join(directory, f))
else:
bundle = Content(os.path.join(directory, f))
except Exception, e:
log("IRREDEEMABLE bundle %-25s (Error: %s)" % (f, e), syslog.LOG_WARNING)
#Clump together bundles of the same ID
x = unique_bundles.setdefault(bundle.bundle_id, [])
x.append(bundle)
all_bundles.append(bundle)
if not show_all_bundles:
#only show the newest one of each set.
bundles = []
for versions in unique_bundles.values():
versions.sort()
bundles.append(versions[-1])
else:
bundles = all_bundles
licenses = {}
for bundle in bundles:
bid = bundle.bundle_id
for k, v in bundle.debug().iteritems():
counts[k] = counts.get(k, 0) + 1
if k.startswith('BAD '):
bc = bad_contents.setdefault(k, {})
bc[bid] = v
for k, v in bundle.linfo.iteritems():
linfo_l = all_linfo.setdefault(k, [])
linfo_l.append(bundle)
for x in v:
linfo_keys[x] = linfo_keys.get(x, 0) + 1
if v['name'] != bundle.name:
linfo_l = unique_linfo.setdefault(k, [])
linfo_l.append(bundle)
if bundle.license:
lic = licenses.setdefault(bundle.license, [])
lic.append(bundle.bundle_id)
citems = counts.items()
rare_keys = [k for k, v in citems if v < 10]
lack_counts = dict((k, v) for k, v in citems if k.startswith('LACKS '))
bad_counts = dict((k, v) for k, v in citems if k.startswith('BAD '))
no_counts = dict((k, v) for k, v in citems if k.startswith('NO '))
tag_counts = dict((k, v) for k, v in citems if k not in lack_counts and
k not in bad_counts and k not in no_counts and k != 'MISSING KEYS')
# flag whether the tag is needed, ok, or not
tag_quality = dict((k, '*') for k in REQUIRED_TAGS)
tag_quality.update((k, '+') for k in OPTIONAL_TAGS)
linfo_counts = dict((k, len(v)) for k, v in all_linfo.iteritems())
linfo_uniq_counts = dict((k, len(v)) for k, v in unique_linfo.iteritems())
log('\nFound: %s bundles\n %s unique bundles' % (len(all_bundles), len(unique_bundles)))
for d, name, d2 in [(tag_counts, '\nattribute counts:', tag_quality),
(lack_counts, '\nmissing required keys:', {}),
(no_counts, '\nunused optional keys:', {}),
(bad_counts, '\nill-formed values:', {}),
(linfo_counts, '\nlinfo counts: total localised', linfo_uniq_counts),
(linfo_keys, '\nlinfo keys:', {})]:
log(name)
counts_reversed = [(v, k) for (k, v) in d.iteritems()]
counts_reversed.sort()
counts_reversed.reverse()
for (k, v) in counts_reversed:
log("%-25s %4s %4s" % (v, k, d2.get(v, '')))
log("\nRare keys:")
for k in rare_keys:
if k.startswith('BAD '):
continue
log(k)
for b in bundles:
v = b.debug().get(k)
if v:
log(' %-25s %s' % (b.bundle_id, v))
log("\nInteresting contents:")
for k, v in bad_contents.iteritems():
log(k)
for x in v.iteritems():
log(' %s: %s' % x)
log("\nInteresting linfo:")
for k in ('pseudo',):
log(k)
for a in all_linfo[k]:
if a in unique_linfo.get(k, []):
log(' * %s (%s vs. %s)' % (a.bundle_id, a.name, a.linfo[k]['name']))
else:
log(' %s (%s)' % (a.bundle_id, a.name))
log("\nLicenses:")
for lic, v in licenses.iteritems():
log("%-20s %s" %(repr(lic), len(v)))
log("\nRare licenses:")
for lic, v in licenses.iteritems():
if len(v) < 3:
log(' %s' % lic)
for x in v:
log(" %s" %(x))
log("\nAlmost valid activities:")
for b in bundles:
d = b.debug()
if d['MISSING KEYS'] == 1:
missing = ', '.join(x for x in d if x.startswith('LACKS'))
bad_values = ', '. join(x for x in d if x.startswith('BAD'))
log("%-20s %s %s" %(b.name, missing, bad_values))
log("\nValid activities (maybe):")
for b in bundles:
d = b.debug()
bid = b.bundle_id
if (d['MISSING KEYS'] == 0 and
bid not in bad_contents['BAD mime_types']):
log("%-20s - %s" %(b.name, bid))
#log(a.raw_data)
def read_info_file(zipfile, path, section):
"""Return a dictionary matching the contents of the config file at
path in zipfile"""
cp = SafeConfigParser()
info = StringIO(zipfile.read(path))
cp.readfp(info)
return dict(cp.items(section))
def canonicalise(lang):
"""Make all equivalent language strings the same.
>>> canonicalise('Zh-cN')
zh-CN
>>> canonicalise('zh_CN')
zh-CN
"""
lang = lang.replace('_', '-').upper()
bits = lang.split('-', 1)
bits[0] = bits[0].lower()
return '-'.join(bits)
def locale_search_path(locale):
"""Find a series of sensible locales to try, including
DEFAULT_LANG. For example 'zh-CN' would become ('zh-CN', 'zh',
'DEFAULT_LANG')."""
#XXX might be better to be storing locale as tuple
if '-' in locale:
return (locale, locale.split('-')[0], DEFAULT_LANG)
return (locale, DEFAULT_LANG)
def read_metadata(bundle_dir):
"""Attempt to read data in a metadata file. Raises expected
exceptions if the metadata file isn't readable. The file should
look something like this:
[org.laptop.Pippy]
description = Succinct description of this activity.
[org.laptop.Develop]
description = Succinct description of this activity.
web_icon = develop.png
"""
md_files = [os.path.join(bundle_dir, x)
for x in os.listdir(bundle_dir) if x.endswith('.info')]
cp = SafeConfigParser()
cp.read(md_files)
metadata = {}
for section in cp.sections():
metadata[section] = dict(x for x in cp.items(section))
return metadata
def htmlise_bundles(bundle_dir, dest_html):
"""Makes a nice html manifest for the bundles in a directory. The
manifest only shows the newest version of each bundle.
"""
#so, we collect up a dictionary of lists, then sort each list on
#the version number to find the newest.
bundles = [os.path.join(bundle_dir, x)
for x in os.listdir(bundle_dir) if x.endswith('.xo') or x.endswith('.xol')]
try:
metadata = read_metadata(bundle_dir)
except Exception, e:
log("had trouble reading metadata: %s" % e)
metadata = {}
all_bundles = {}
for filename in bundles:
try:
if filename.endswith('.xo'):
bundle = Activity(filename)
else:
bundle = Content(filename)
x = all_bundles.setdefault(bundle.bundle_id, [])
x.append((bundle.mtime, bundle))
except Exception, e:
log("Couldn't find good activity/library info in %s (Error: %s)" % (filename, e))
newest = []
# create an index for each language that has a template
# but track any locales in bundles in case we do not have templates for them
locales = [os.path.join(o) for o in os.listdir(TEMPLATE_DIR) if os.path.isdir(os.path.join(TEMPLATE_DIR,o))]
locales_found = set ()
for versions in all_bundles.values():
versions = [x[1] for x in sorted(versions)]
# end of list is the newest; beginning of list might need deleting
latest = versions.pop()
locales_found.update(latest.linfo)
newest.append(latest)
goners = versions[:-KEEP_OLD_VERSIONS]
keepers = versions[-KEEP_OLD_VERSIONS:]
for v in goners:
fn = os.path.join(bundle_dir, v.url)
os.remove(fn)
latest.set_older_versions(keepers)
if latest.bundle_id in metadata:
# we have extra metadata with which to fill out details
# presumably this is mainly human-oriented description.
d = metadata[latest.bundle_id]
for k in ('description', 'name'):
if k in d:
setattr(latest, k, d[k])
log('found locales: %s' % locales)
# assume locales is not empty as we have at least the default language
for locale in locales:
try:
make_html(newest, locale, '%s.%s' % (dest_html, locale))
except Exception, e:
log("Couldn't make page for %s (Error: %s)" % (locale, e), syslog.LOG_WARNING)
# make_varfile(locales, dest_html)- have switched to multiviews, so var not needed
def make_varfile(locales, dest_html):
f = open(dest_html + '.var', 'w')
index = os.path.basename(dest_html)
f.write('URI: %s\n\n' % index)
for locale in locales:
f.write('URI: %s.%s\n' % (index, locale))
f.write('Content-type: text/html; charset=utf-8\n')
f.write('Content-language: %s\n\n' % locale)
# now the default, slightly higher qs
f.write('URI: %s.DEFAULT\n' % index)
f.write('Content-type: text/html; charset=utf-8\n')
f.write('Content-language: en\n\n')
f.close()
def read_template(name, locale):
"""Try to read the locale's template, falling back to the
default."""
#also try containing locales, eg 'zh' for 'zh-CN'
for x in locale_search_path(locale):
try:
f = open(os.path.join(TEMPLATE_DIR, x, name))
break
except (OSError, IOError), e:
#log(str(e))
continue
s = f.read()
f.close()
return s
def make_html(bundles, locale, filename):
"""Write a microformated index for the activities in the appropriate language,
and save it to filename."""
page_tmpl = read_template('page', locale)
act_tmpl = read_template('activity', locale)
#bundles.sort() won't cut it.
schwartzian = [ (x.get_name(locale), x.to_html(locale, act_tmpl)) for x in bundles ]
schwartzian.sort()
s = page_tmpl % {'activities': '\n'.join(x[1] for x in schwartzian)}
if os.path.exists(filename):
shutil.move(filename, filename + '~')
f = open(filename, 'w')
f.write(s)
f.close()

View file

@ -0,0 +1,134 @@
# assume apache in admin group
- name: Create xs-activity-server directory tree
file: path={{ item }}
mode=0755
owner=root
group=admin
state=directory
with_items:
- /library/xs-activity-server
- /library/xs-activity-server/activities
- /library/xs-activity-server/lang_templates
- /library/xs-activity-server/www.0
- /library/xs-activity-server/tmp
# Wish synchronize worked, but it doesn't
- name: Copy language templates
command: rsync -a {{ iiab_dir }}/roles/activity-server/files/lang_templates /library/xs-activity-server/
- name: Copy default index files
copy: src={{ item }}
dest=/library/xs-activity-server/www.0
mode=0755
owner=root
group=root
with_fileglob:
- www.0/index.html.*
- name: Point www to www.0 as default
file: src=/library/xs-activity-server/www.0
dest=/library/xs-activity-server/www
owner=root
group=admin
state=link
- name: Chown language templates
file: path=/library/xs-activity-server/lang_templates
mode=0644
owner=root
group=admin
state=directory
recurse=yes
# We should have a var for python site-packages directory
- name: Create xs-activity-server python site-packages directory
file: path=/usr/lib/python2.7/site-packages/xs_activities
mode=0755
owner=root
group=root
state=directory
- name: Install Python module
copy: src=xs_activities/__init__.py
dest=/usr/lib/python2.7/site-packages/xs_activities
mode=0644
owner=root
group=root
- name: Copy scripts to /usr/bin
copy: src={{ item }}
dest=/usr/bin
mode=0755
owner=root
group=root
with_items:
- bin/xs-regenerate-activities
- bin/xs-check-activities
# Do in ansible what was done in /etc/sysconfig/olpc-scripts/setup.d/xs-activity-server script
- name: Copy xs-activity-server config file
template: src=xs-activity-server.conf
dest=/etc/{{ apache_config_dir }}
owner=root
group=root
mode=0644
# SEE ALSO THE apache2_module SECTION IN roles/httpd/tasks/main.yml
- name: enable mod_expires for debian
command: a2enmod expires
when: is_debuntu | bool
- name: create the link which enables the site
file: src=/etc/apache2/sites-available/xs-activity-server.conf
dest=/etc/apache2/sites-enabled/xs-activity-server.conf
state=link
when: activity_server_enabled and is_debuntu
- name: delete the link which enables the site
file: src=/etc/apache2/sites-available/xs-activity-server.conf
dest=/etc/apache2/sites-enabled/xs-activity-server.conf
state=absent
when: not activity_server_enabled and is_debuntu
- name: Copy xs-activity-server usbmount file
template: src=usbmount-60-xs-activity-server-installcontent
dest=/etc/usbmount/mount.d
owner=root
group=root
mode=0755
# TODO: Fix multiview so it supports portal language menu
# For it only supports client's language code
# TODO: Upload Activity via web interface
# and figure out what to do with olpc_activities.service
# short term addition of link for upload-activity server
# ln -sf /usr/share/xs-config/cfg/html/top/en/cntr_upl_activity.php {{ doc_root }}/upload_activity.php
- name: Restart httpd
service: name={{ apache_service }}
enabled=yes
state=restarted
- name: Add 'activity-server' variable values to {{ iiab_ini_file }}
ini_file:
path: "{{ iiab_ini_file }}"
section: activity-server
option: "{{ item.option }}"
value: "{{ item.value | string }}"
with_items:
- option: name
value: "Activity Server"
- option: description
value: "Download an Activity."
- option: path
value: /activities
- option: enabled
value: "{{ xo_services_enabled }}"

View file

@ -0,0 +1,115 @@
#!/bin/bash
# Part of the xs-activity-server package
#
# based on a similarly named script in the xs-rsync package
# by Martin Langhoff <martin@laptop.org>
#
# Adapted for xs-activity-server by Douglas Bagnall
# <douglas@paradise.net.nz>
#
# Copyright: One Laptop per Child
set -e
VERBOSE=yes
MAGIC_DIR=$UM_MOUNTPOINT/xs-activity-server
FINAL_DIR=/library/xs-activity-server/activities
FILES_TO_RM=""
# combined with set -e, error() is called if something fails.
error(){
logger -puser.err -t "xs-activity-server[$$]" "Error at line $(caller)"
[ "$FILES_TO_RM" ] && rm -rf $FILES_TO_RM
}
trap error ERR
# Log a string via the syslog facility.
log()
{
if test $1 != debug || expr "$VERBOSE" : "[yY]" > /dev/null; then
logger -p user.$1 -t "xs-activity-server[$$]" -- "$2"
fi
}
STEPS=7
[ -d $MAGIC_DIR ] || exit 0
log notice 'Found activity install directory';
log notice "[1/$STEPS] Checking whether it has a manifest";
if [ -r $MAGIC_DIR/manifest.sha1 ];then
log notice "[2/$STEPS] Seems to have a manifest";
else
log err "[2/$STEPS] Missing manifest"
exit 1;
fi
## Do we have enough space?
# note: we could use awk {'print $4'} instead of the
# perl regex, but it breaks with long /dev nodes
# such as those from LVMs -which wrap. The regex captures the
# number just left of the number with the percentage sign.
NEED=`du -s -B1M $MAGIC_DIR | awk {'print $1'}`
HAVE=`df -B1M $FINAL_DIR | tail -n1 | \
perl -pe 'm/(\d+)\s+\d+\%/; $_=($1-1);'`
if [ $NEED -gt $HAVE ];then
log err 'Not enough free space in /library for these activities - cancelling';
exit 1;
fi
### Copy it first - as the media is bound to be slow
# - make this atomic by cp'ing to a tmpdir, and mv'ing into place
# to be fail-safe
# - mv aside manifest.sha1 and its sig
# - TODO? we could avoid cp'ing files we already have using
# rsync --copy-dest instead of cp
#
log notice "[3/$STEPS] Copying activities to disk";
TMPDEST=`mktemp -d -p /library/xs-activity-server/tmp`
#make sure the tmp directory goes
FILES_TO_RM="$FILES_TO_RM '$TMPDEST'"
cp --preserve=timestamps $MAGIC_DIR/* $TMPDEST
# In a tmpdir we own, safe from race conditions
# run the checksums...
log notice "[4/$STEPS] Checking the manifest";
# mv the manifest to a different dir
TMPMANIF=`mktemp -d -p /library/xs-activity-server/tmp`
FILES_TO_RM="$FILES_TO_RM '$TMPMANIF'"
mv $TMPDEST/manifest.sha1 $TMPMANIF/
if [ -e $TMPDEST/manifest.sha1.sig ]; then
mv $TMPDEST/manifest.sha1.sig $TMPMANIF/
fi
xs-sum -c $TMPMANIF/manifest.sha1 -d $TMPDEST
#Let syslog know what we're doing
cd $TMPDEST
log notice "found $(ls *.xo |wc -l) activities"
log debug "found these activities: $(ls *.xo)"
cd -
log notice "[5/$STEPS] Copy the directories into place";
#XXX not checking whether this clobbers existing files.
mv $TMPDEST/* $FINAL_DIR
#So, now all the activities are in place, but maybe they're not
#newer than what we have. So xs-regenerate-activities has to work that out.
log notice "[6/$STEPS] Regenerating the list of available activities";
/usr/bin/xs-regenerate-activities $FINAL_DIR 2>&1 | logger -p user.debug -t "xs-activity-server[$$]"
log notice "[$STEPS/$STEPS] Finished - XOs can now update activities.";
rm -fr $FILES_TO_RM

View file

@ -0,0 +1,30 @@
# xs-activity-server
#
# Copyright: On Laptop per Child
# GPL v2
# written by Douglas Bagnall <douglas@paradise.net.nz>
#
# This belongs in the apache conf.d directory.
# (probably /etc/httpd/conf.d/)
Alias /activities /library/xs-activity-server/www
<Directory /library/xs-activity-server/www >
# Languages are set in 010-iiab.conf
ExpiresActive On
ExpiresDefault now
Options +MultiViews
Require all granted
#NOTE: an index.html.var file is generated, which ought to make
# MultiViews redundant (by my reading) but it doesn't seem to
# work. Someone could look at that sometime.
</Directory>
#<Directory /activities >
# ExpiresActive On
# ExpiresDefault now
#</Directory>
#<Location /activities >
# ExpiresActive On
# ExpiresDefault now
#</Location>