Commit bb75417a authored by BAIRE Anthony's avatar BAIRE Anthony
Browse files

Merge branch 'django' of gitlab.inria.fr:allgo/allgo into 238-download-file-with-nginx

# Conflicts:
#	django/allgo/main/views.py
parents 275528b1 0fc5a9f2
Pipeline #38600 failed with stage
in 1 minute and 8 seconds
#!/bin/bash
CONTAINERS="dev-mysql dev-controller dev-ssh dev-rails dev-django dev-nginx dev-smtpsink dev-registry"
CONTAINERS="dev-mysql dev-controller dev-ssh dev-rails dev-django dev-smtpsink dev-registry dev-nginx"
die()
......@@ -35,21 +35,21 @@ EOF
install_secrets()
{
# install the tokens certificate into the registry external volume
src=data/django/ro/certs/tokens.crt
dst=data/registry/ro/certs/tokens.crt
if [ -f "$src" ] && [ -d "`dirname "$dst"`" ] && ! cmp -s "$src" "$dst" ; then
local src=data/django/ro/certs/tokens.crt
local dst=data/registry/ro/certs/tokens.crt
if [ -f "$src" ] && [ -d "`dirname "$dst"`" ] && [[ "$TODO" =~ dev-django|dev-registry ]] ; then
echo "Install tokens certificate as $dst/tokens.crt"
cp "$src" "$dst"
docker-compose restart dev-registry
fi
# install the controller token into the controller external volume
src=data/django/ro/controller_token
dst=data/controller/ro/config.yml
if [ -f "$src" ] && [ -f "$dst" ] && [ "$src" -nt "$dst" ] ; then
local src=data/django/ro/controller_token
local dst=data/controller/ro/config.yml
if [ -f "$src" ] && [ -f "$dst" ] && [[ "$TODO" =~ dev-django|dev-controller ]] ; then
echo "Install controller token into $dst"
docker exec -i dev-controller sh -c "cat >>/vol/ro/config.yml" <<EOF
# copy the file with a docker command because it is owned by root
docker run --rm -i -v "$PWD/$dst:/config.yml" busybox sh -c "cat >>/config.yml" <<EOF
registry_auth: {"username": "\$token", "password": "`cat "$src"`"}
EOF
docker-compose restart dev-controller
......@@ -68,10 +68,12 @@ purge_container()
local data_dir="data/${name/dev-/}"
if [ -e "$data_dir" ] ; then
echo "warning: data dir '$data_dir' already exists"
echo -n "remove it [y/N]? "
read confirm
[ "$confirm" = "y" ] || die "aborted"
if [ -z "$FORCE" ] ; then
echo "warning: data dir '$data_dir' already exists"
echo -n "remove it [y/N]? "
read confirm
[ "$confirm" = "y" ] || die "aborted"
fi
(set -x ; rm -rf -- "$data_dir")
fi
......@@ -103,6 +105,18 @@ init_container()
)
}
# seed the db & registry
seed()
{
if [ -z "$NOSEED" ] ; then
if [[ "$TODO" =~ "dev-django" ]] ; then
django/tools/seed-dev.sh --django dev localhost:5000
fi
if [[ "$TODO" =~ "dev-registry" ]] ; then
django/tools/seed-dev.sh --registry dev localhost:5000
fi
fi
}
##################################################################
......@@ -113,7 +127,7 @@ fi
if [ "$1" == "-h" ] ; then
cat <<EOF
usage: $0 [-n|--nobuild] [CONTAINER ...]
usage: $0 [-n|--nobuild|--noseed] [CONTAINER ...]
The bootstrap script initialises the environment and the selected containers
(or by default all the containers).
......@@ -123,16 +137,38 @@ before purging its data (to bootstrap it again).
Options:
-n,--nobuild do not rebuild the images
--noseed do not seed the db & registry
--nostart do not start the containers at the end (actually this stops the
containers at the end of the process because they need to be
started to be initialised)
--force do not ask for confirmation before deleting data (removing
external volumes when bootstraping a container over an existing one)
EOF
fi
if [ "$1" == "-n" ] || [ "$1" = "--nobuild" ] ; then
NOBUILD=1
shift
else
NOBUILD=
fi
NOBUILD=
NOSEED=
NOSTART=
FORCE=
while true ; do
case "$1" in
-n|--nobuild)
NOBUILD=1
shift;;
--noseed)
NOSEED=1
shift;;
--nostart)
NOSTART=1
shift;;
--force)
FORCE=1
shift;;
*)
break;;
esac
done
# selection of the containers to be generated
if [ -n "$*" ] ; then
......@@ -168,5 +204,16 @@ done
install_secrets
# force restarting the nginx frontend because the IP address of
# dev-django/dev-registry may have changed (and nginx does not support IP
# address changes in upstream servers)
docker-compose restart dev-nginx
seed
if [ -n "$NOSTART" ] ; then
docker-compose down
fi
# display running containers
docker-compose ps
......@@ -63,6 +63,7 @@ default_executor = ThreadPoolExecutor(10)
# job log
REDIS_KEY_JOB_LOG = "log:job:%d"
REDIS_KEY_JOB_STATE = "state:job:%d"
REDIS_KEY_JOB_RESULT = "result:job:%d"
# pubsub channels for waking up allgo.aio (frontend) and the controller
# (ourselves)
......@@ -227,6 +228,26 @@ async def _make_aiohttp_client(host):
def make_aiohttp_client(host):
return asyncio.get_event_loop().run_until_complete(_make_aiohttp_client(host))
class DockerAuthConfigDict(dict):
"""dict of authentication credentials for docker.Client
Keys: repository names (or possibly subrepositories)
Values: auth config dicts (suitable for docker.Client.{pull,push})
"""
def get_auth_config(self, image):
"""Get the suitable authentication credential for a given docker_image
Return a 'auth_config' dict or None if no credentials are found
"""
for registry, auth_config in self.items():
if image.startswith(registry):
l = len(registry)
if image[l:l+1] in ("", "/", ":"):
log.debug("select auth_config %r for image %r", registry, image)
return auth_config
log.debug("select no auth_config for image %r", image)
class Manager:
"""A class for scheduling asynchronous jobs on a collection of keys
......@@ -992,6 +1013,8 @@ exec /.toolbox/bin/sshd -D
ses.execute("UPDATE dj_webapps SET sandbox_state=%d WHERE id=%d AND sandbox_state=%d" %
(next_state, webapp_id, webapp.sandbox_state))
yield from self.ctrl.notif_webapp_updated(webapp_id)
log.debug("done sandbox %d", webapp_id)
......@@ -1009,6 +1032,10 @@ class JobManager(Manager):
raise NotImplementedError()
# create the job and:
# - return True if started
# - return False if cancelled by the user
# - raise exception on error
def _create_job(self, info):
ctrl = self.ctrl
ses = ctrl.session
......@@ -1018,14 +1045,19 @@ class JobManager(Manager):
try:
with ses.begin():
# atomically switch the job state from WAITING to STARTING
# (because the UI may destroy the job before it is started)
if ses.execute("UPDATE dj_jobs SET state=%d WHERE id=%d AND state=%d"
% (JobState.RUNNING, info.job_id, JobState.WAITING)
).rowcount == 0:
log.info("job %d not started (destroyed by user)", info.job_id)
return False
job = ses.query(Job).filter_by(id=info.job_id).one()
webapp = job.webapp
log.info("start job %d (%s:%s)",
info.job_id, webapp.docker_name, info.version)
job.state = int(JobState.RUNNING) # pragma: nobranch (TODO: remove (coverage bug))
repo = ctrl.gen_image_name(webapp)
image = "%s:%s" % (repo, info.version)
......@@ -1130,6 +1162,7 @@ class JobManager(Manager):
with ses.begin():
# save the container_id into the db
job.container_id = info.ctr_id
return True
except:
#TODO introduce a state JobState.ERROR
......@@ -1392,8 +1425,8 @@ class JobManager(Manager):
job.state = int(JobState.DONE)
# TODO report error to the user
job.result= int(JobResult.ERROR)
return
info.ver_id = ver.id
else:
info.ver_id = ver.id
elif state in (JobState.RUNNING, JobState.ABORTING): # pragma: nobranch
# job is already started
......@@ -1429,11 +1462,6 @@ class JobManager(Manager):
info.timeout -= uptime
log.debug("job %d uptime: %.1fs, adjusted timeout is: %.1f",
info.job_id, uptime, info.timeout)
else:
# unexpected state
if state != JobState.DONE:
log.warning("job id %d is in unexpected state %s", job_id, state.name)
return
if state == JobState.WAITING:
......@@ -1449,7 +1477,9 @@ class JobManager(Manager):
# request a slot from the shared swarm
with info.client.request_slot(info.ctr_name, info.cpu or 0, info.mem or 0):
info.node_id = yield from info.client.wait_slot(info.ctr_name)
yield from self.run_in_executor(self._create_job, info, lock=False)
if not (yield from self.run_in_executor(self._create_job, info, lock=False)):
# cancelled by user
return
yield from self._notif_job_state(info, "RUNNING")
yield from self._finish_job(info, reset)
......@@ -1458,16 +1488,30 @@ class JobManager(Manager):
# -> wait for its termination
yield from self._finish_job(info, reset)
yield from self._notif_job_state(info, "DONE")
# replicate the job status/result in the redis database (this is needed
# for transitions to state DONE, but also to DELETED and ARCHIVED
# because the django server does not write any job state in redis)
with ses.begin():
job = ses.query(Job).filter_by(id=job_id).first()
if job is None:
state, result = "DELETED", None
else:
state = JobState (job.state) .name
result = JobResult(job.result).name
yield from self._notif_job_state(info, state, result)
# send a notification to the aio frontend when the job state is changed
#
# this function never fail (redis errors are caught and written in the
# logs)
async def _notif_job_state(self, info, state):
async def _notif_job_state(self, info, state, result=None):
try:
log.info("notify the frontend: job %d state is now %r",
info.job_id, state)
log.info("notify the frontend: job %d state is now %r with result %r",
info.job_id, state, result)
if result is not None:
await self.ctrl.redis_client.setex(REDIS_KEY_JOB_RESULT % info.job_id,
86400, result)
await self.ctrl.redis_client.setex(REDIS_KEY_JOB_STATE % info.job_id,
86400, state)
await self.ctrl.redis_client.publish(REDIS_CHANNEL_AIO,
......@@ -1489,25 +1533,27 @@ class JobManager(Manager):
#
class PullManager(Manager):
def __init__(self, nb_threads, client, name, *, auth_config=None):
def __init__(self, nb_threads, client, name, *,
auth_dict=DockerAuthConfigDict()):
super().__init__(nb_threads, interruptible=True)
self.client = client
self.name = name
self.auth_config = auth_config
self.auth_dict = auth_dict
@asyncio.coroutine
def _process(self, img, reset, rescheduled):
image, version = img
log.info("pull to the %-10s %s:%s", self.name, image, version)
return self.run_in_executor(lambda:
self.client.pull(image, version, auth_config=self.auth_config))
self.client.pull(image, version,
auth_config=self.auth_dict.get_auth_config(image)))
class PushManager(Manager):
def __init__(self, nb_threads, ctrl, *, auth_config=None):
def __init__(self, nb_threads, ctrl, *, auth_dict=DockerAuthConfigDict()):
super().__init__(nb_threads, interruptible=True)
self.ctrl = ctrl
self.auth_config = auth_config
self.auth_dict = auth_dict
@asyncio.coroutine
def _process(self, version_id, reset, rescheduled):
......@@ -1540,8 +1586,23 @@ class PushManager(Manager):
tag = version.number
log.info("push from the %-8s %s:%s", "sandbox", image, tag)
yield from self.run_in_executor(docker_check_error, lambda:
self.ctrl.sandbox.push(image, tag, auth_config=self.auth_config))
# FIXME: docker-py folks are morons in 1.9.0 they added the
# 'auth_config' argument for .pull() but not for .push()
# thus we have to use .login() instead (but I don't want to
# do a global .login() at startup because this may fail if
# the registry is down)
#
# the following code will work w/ versions >=1.10
#yield from self.run_in_executor(lambda: docker_check_error(
# self.ctrl.sandbox.push, image, tag,
# auth_config=self.auth_dict.get_auth_config(image)))
#
def do_push():
auth_config = self.auth_dict.get_auth_config(image)
self.ctrl.sandbox.login(auth_config["username"], auth_config["password"],
registry=image)
docker_check_error(self.ctrl.sandbox.push, image, tag)
yield from self.run_in_executor(do_push)
reset()
......@@ -1571,16 +1632,16 @@ class ImageManager:
nb_push_sandbox = NB_PUSH_SANDBOX,
nb_pull_sandbox = NB_PULL_SANDBOX,
nb_pull_swarm = NB_PULL_SWARM,
*, auth_config=None):
*, auth_dict=None):
self.ctrl = ctrl
self.sandbox_push_manager = PushManager(nb_push_sandbox, ctrl,
auth_config=auth_config)
auth_dict=auth_dict)
self.sandbox_pull_manager = PullManager(nb_pull_sandbox, ctrl.sandbox, "sandbox",
auth_config=auth_config)
auth_dict=auth_dict)
self.swarm_pull_manager = PullManager(nb_pull_swarm, ctrl.swarm, "swarm",
auth_config=auth_config)
auth_dict=auth_dict)
# return a future
@auto_create_task
......@@ -1663,10 +1724,10 @@ class DockerController:
self.cpu_shares = cfg.get("cpus", None, int)
dct = cfg.get("registry_auth", None, dict)
auth_config = {
auth_dict = DockerAuthConfigDict({registry: {
"username": dct.get("username", str, required=True),
"password": dct.get("password", str, required=True),
} if dct else None
}} if dct else {})
self.sandbox = SharedSwarmClient(sandbox_host, config=cfg.get("sandbox", {}, dict), alias="sandbox")
self.sandbox.aiohttp_session, self.sandbox.aiohttp_url = (
......@@ -1685,7 +1746,7 @@ class DockerController:
self.redis_host = redis_host
self.redis_client = None
self.image_manager = ImageManager(self, auth_config=auth_config)
self.image_manager = ImageManager(self, auth_dict=auth_dict)
self.sandbox_manager = SandboxManager(self)
self.job_manager = JobManager(self)
......@@ -1898,7 +1959,7 @@ class DockerController:
if item_type == b"job":
self.job_manager.process(item_id)
elif item_type == b"sandbox":
elif item_type == b"webapp":
self.sandbox_manager.process(item_id)
else:
log.warning("ignored notification for unknown item %r", msg)
......@@ -1912,6 +1973,22 @@ class DockerController:
log.exception("error in the redis notification loop")
async def notif_webapp_updated(self, webapp_id):
"""Send a notification to the aio frontend when a webapp is updated
this function never fails (redis errors are caught and written in
the logs)
"""
try:
log.info("notify the frontend: webapp %d updated", webapp_id)
await self.redis_client.publish(REDIS_CHANNEL_AIO,
REDIS_MESSAGE_WEBAPP_UPDATED % webapp_id)
except asyncio.CancelledError:
pass
except Exception as e:
log.error("redis notification failed for webapp %d (%e)",
webapp_id, e)
def shutdown(self):
if not self._shutdown_requested.done():
self._shutdown_requested.set_result(None)
......
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="114.71908mm"
height="114.71908mm"
viewBox="0 0 114.71908 114.71908"
version="1.1"
id="svg1633"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="allgo-logo-wo-border.svg">
<defs
id="defs1627" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="268.22053"
inkscape:cy="96.791954"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata1630">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(119.34763,-59.723793)">
<path
id="path1604"
d="m -83.279301,75.475537 c -4.160776,0 -7.704997,1.464027 -10.632943,4.39198 -2.92795,2.927955 -4.391977,6.472164 -4.391977,10.632944 v 68.190669 h 12.482431 v -29.12536 h 12.494326 V 117.08333 H -85.82179 V 90.500461 c 0,-0.69346 0.2312,-1.271429 0.693502,-1.733751 0.539358,-0.539359 1.155528,-0.808728 1.848987,-0.808728 h 9.951837 V 75.475537 Z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:115.57705688px;line-height:1.25;font-family:Orbitron;-inkscape-font-specification:'Orbitron, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:2.88941789;stroke-opacity:1"
inkscape:connector-curvature="0"
inkscape:export-xdpi="22.139999"
inkscape:export-ydpi="22.139999" />
<path
inkscape:connector-curvature="0"
id="path1606"
d="m -50.648767,75.475537 v 12.482445 h 9.951841 c 0.69346,0 1.271442,0.269369 1.733749,0.808728 0.53935,0.462322 0.808728,1.040291 0.808728,1.733751 v 26.582869 h -12.494318 v 12.48244 h 12.494318 v 29.12536 h 12.482436 V 90.500461 c 0,-4.16078 -1.464022,-7.704989 -4.391978,-10.632944 -2.92795,-2.927953 -6.472166,-4.39198 -10.632935,-4.39198 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:115.57705688px;line-height:1.25;font-family:Orbitron;-inkscape-font-specification:'Orbitron, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:2.88941789;stroke-opacity:1"
inkscape:export-xdpi="22.139999"
inkscape:export-ydpi="22.139999" />
<rect
y="70.119789"
x="-73.459923"
height="93.927086"
width="7.9375"
id="rect1608"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.26458332;stroke-opacity:1"
inkscape:export-xdpi="22.139999"
inkscape:export-ydpi="22.139999" />
<rect
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.26458332;stroke-opacity:1"
id="rect1610"
width="7.9375"
height="93.927086"
x="-58.520386"
y="70.119789"
inkscape:export-xdpi="22.139999"
inkscape:export-ydpi="22.139999" />
<rect
ry="10.616675"
y="59.723793"
x="-119.34763"
height="114.71908"
width="114.71908"
id="rect1612"
style="fill:none;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:export-xdpi="22.139999"
inkscape:export-ydpi="22.139999" />
</g>
</svg>
......@@ -10,11 +10,11 @@
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="118.71908mm"
height="261.59412mm"
viewBox="0 0 118.71908 261.59412"
height="118.71908mm"
viewBox="0 0 118.71908 118.71908"
version="1.1"
id="svg8"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="allgo-logo.svg">
<defs
id="defs2" />
......@@ -26,8 +26,8 @@
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.91129566"
inkscape:cx="224.35101"
inkscape:cy="494.35109"
inkscape:cx="0.49391562"
inkscape:cy="74.568709"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
......@@ -43,8 +43,8 @@
<inkscape:grid
type="xygrid"
id="grid4543"
originx="-75.87884"
originy="34.122031" />
originx="-75.878841"
originy="-108.75301" />
</sodipodi:namedview>
<metadata
id="metadata5">
......@@ -63,14 +63,6 @@
inkscape:groupmode="layer"
id="layer1"
transform="translate(-75.878838,-69.527924)">
<rect
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4546"
width="230.92087"
height="133.63477"
x="13.60504"
y="206.15407"
ry="15.213515" />
<flowRoot
xml:space="preserve"
id="flowRoot838"
......@@ -81,75 +73,37 @@
height="442.85715"
x="-185.71428"
y="42.519684" /></flowRegion><flowPara
id="flowPara844" /></flowRoot> <g
id="g4531">
<path
id="path4549"
d="m 113.94718,87.279666 c -4.16078,0 -7.705,1.464027 -10.63295,4.39198 -2.92795,2.927955 -4.391977,6.472165 -4.391977,10.632944 v 68.19067 H 111.40469 V 141.3699 h 12.49433 v -12.48244 h -12.49433 v -26.58287 c 0,-0.69346 0.2312,-1.27143 0.6935,-1.73375 0.53936,-0.53936 1.15553,-0.808729 1.84899,-0.808729 h 9.95184 V 87.279666 Z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:115.57705688px;line-height:1.25;font-family:Orbitron;-inkscape-font-specification:'Orbitron, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:2.88941789;stroke-opacity:1"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path4537"
d="m 146.57773,87.279666 v 12.482445 h 9.95185 c 0.69346,0 1.27144,0.269369 1.73375,0.808729 0.53935,0.46232 0.80873,1.04029 0.80873,1.73375 v 26.58287 h -12.49433 v 12.48244 h 12.49433 v 29.12536 h 12.48244 v -68.19067 c 0,-4.160779 -1.46402,-7.704989 -4.39198,-10.632944 -2.92795,-2.927953 -6.47217,-4.39198 -10.63294,-4.39198 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:115.57705688px;line-height:1.25;font-family:Orbitron;-inkscape-font-specification:'Orbitron, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:2.88941789;stroke-opacity:1" />
<rect
y="81.92392"
x="123.76653"
height="93.927086"
width="7.9375"
id="rect4545"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.26458332;stroke-opacity:1" />
<rect
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.26458332;stroke-opacity:1"
id="rect4551"
width="7.9375"
height="93.927086"
x="138.70607"
y="81.92392" />
<rect
ry="10.616675"
y="71.527924"
x="77.878838"
height="114.71908"
width="114.71908"
id="rect4565"
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
<g
id="g4558">
<path