Skip to content

Commit

Permalink
* Scrubber now uses registry client to communicate with registry
Browse files Browse the repository at this point in the history
* glance-api writes out to a scrubber "queue" dir on delete
* Scrubber determines images to deleted from "queue" dir not db

Change-Id: Ia5574fc75f1a9c763bdef0f5773c2c182932b68a
  • Loading branch information
jkoelker committed Sep 12, 2011
1 parent d5099cc commit 27b3df2
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 110 deletions.
7 changes: 7 additions & 0 deletions etc/glance-api.conf
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ image_cache_stall_timeout = 86400
# Turn on/off delayed delete
delayed_delete = False

# Delayed delete time in seconds
scrub_time = 43200

# Directory that the scrubber will use to remind itself of what to delete
# Make sure this is also set in glance-scrubber.conf
scrubber_datadir = /var/lib/glance/scrubber

[pipeline:glance-api]
pipeline = versionnegotiation context apiv1app
# NOTE: use the following pipeline for keystone
Expand Down
33 changes: 16 additions & 17 deletions etc/glance-scrubber.conf
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,27 @@ log_file = /var/log/glance/scrubber.log
# Send logs to syslog (/dev/log) instead of to file specified by `log_file`
use_syslog = False

# Delayed delete time in seconds
scrub_time = 43200

# Should we run our own loop or rely on cron/scheduler to run us
daemon = False

# Loop time between checking the db for new items to schedule for delete
# Loop time between checking for new items to schedule for delete
wakeup_time = 300

# SQLAlchemy connection string for the reference implementation
# registry server. Any valid SQLAlchemy connection string is fine.
# See: http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/connections.html#sqlalchemy.create_engine
sql_connection = sqlite:///glance.sqlite

# Period in seconds after which SQLAlchemy should reestablish its connection
# to the database.
#
# MySQL uses a default `wait_timeout` of 8 hours, after which it will drop
# idle connections. This can result in 'MySQL Gone Away' exceptions. If you
# notice this, you can lower this value to ensure that SQLAlchemy reconnects
# before MySQL can drop the connection.
sql_idle_timeout = 3600
# Directory that the scrubber will use to remind itself of what to delete
# Make sure this is also set in glance-api.conf
scrubber_datadir = /var/lib/glance/scrubber

# Only one server in your deployment should be designated the cleanup host
cleanup_scrubber = False

# pending_delete items older than this time are candidates for cleanup
cleanup_scrubber_time = 86400

# Address to find the registry server for cleanups
registry_host = 0.0.0.0

# Port the registry server is listening on
registry_port = 9191

[app:glance-scrubber]
paste.app_factory = glance.store.scrubber:app_factory
9 changes: 8 additions & 1 deletion glance/common/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,21 @@ def __init__(self, auth_tok=None, user=None, tenant=None, roles=None,
self.roles = roles or []
self.is_admin = is_admin
self.read_only = read_only
self.show_deleted = show_deleted
self._show_deleted = show_deleted
self.owner_is_tenant = owner_is_tenant

@property
def owner(self):
"""Return the owner to correlate with an image."""
return self.tenant if self.owner_is_tenant else self.user

@property
def show_deleted(self):
"""Admins can see deleted by default"""
if self._show_deleted or self.is_admin:
return True
return False


class ContextMiddleware(wsgi.Middleware):
def __init__(self, app, options):
Expand Down
17 changes: 17 additions & 0 deletions glance/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"""

import datetime
import errno
import inspect
import logging
import os
Expand Down Expand Up @@ -133,6 +134,22 @@ def parse_isotime(timestr):
return datetime.datetime.strptime(timestr, TIME_FORMAT)


def safe_mkdirs(path):
try:
os.makedirs(path)
except OSError, e:
if e.errno != errno.EEXIST:
raise


def safe_remove(path):
try:
os.remove(path)
except OSError, e:
if e.errno != errno.ENOENT:
raise


class LazyPluggable(object):
"""A pluggable backend loaded lazily based on some value."""

Expand Down
77 changes: 37 additions & 40 deletions glance/registry/db/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,15 @@ def image_get(context, image_id, session=None):
raise exception.NotFound("No image found")

try:
image = session.query(models.Image).\
options(joinedload(models.Image.properties)).\
options(joinedload(models.Image.members)).\
filter_by(deleted=_deleted(context)).\
filter_by(id=image_id).\
one()
query = session.query(models.Image).\
options(joinedload(models.Image.properties)).\
options(joinedload(models.Image.members)).\
filter_by(id=image_id)

if not can_show_deleted(context):
query = query.filter_by(deleted=False)

image = query.one()
except exc.NoResultFound:
raise exception.NotFound("No image found with ID %s" % image_id)

Expand All @@ -161,30 +164,6 @@ def image_get(context, image_id, session=None):
return image


def image_get_all_pending_delete(context, delete_time=None, limit=None):
"""Get all images that are pending deletion
:param limit: maximum number of images to return
"""
session = get_session()
query = session.query(models.Image).\
options(joinedload(models.Image.properties)).\
options(joinedload(models.Image.members)).\
filter_by(deleted=True).\
filter(models.Image.status == 'pending_delete')

if delete_time:
query = query.filter(models.Image.deleted_at <= delete_time)

query = query.order_by(desc(models.Image.deleted_at)).\
order_by(desc(models.Image.id))

if limit:
query = query.limit(limit)

return query.all()


def image_get_all(context, filters=None, marker=None, limit=None,
sort_key='created_at', sort_dir='desc'):
"""
Expand All @@ -204,9 +183,16 @@ def image_get_all(context, filters=None, marker=None, limit=None,
query = session.query(models.Image).\
options(joinedload(models.Image.properties)).\
options(joinedload(models.Image.members)).\
filter_by(deleted=_deleted(context)).\
filter(models.Image.status != 'killed')

if not can_show_deleted(context) or 'deleted' not in filters:
query = query.filter_by(deleted=False)
else:
query = query.filter_by(deleted=filters['deleted'])

if 'deleted' in filters:
del filters['deleted']

sort_dir_func = {
'asc': asc,
'desc': desc,
Expand Down Expand Up @@ -469,11 +455,15 @@ def image_member_get(context, member_id, session=None):
"""Get an image member or raise if it does not exist."""
session = session or get_session()
try:
member = session.query(models.ImageMember).\
query = session.query(models.ImageMember).\
options(joinedload(models.ImageMember.image)).\
filter_by(deleted=_deleted(context)).\
filter_by(id=member_id).\
one()
filter_by(id=member_id)

if not can_show_deleted(context):
query = query.filter_by(deleted=False)

member = query.one()

except exc.NoResultFound:
raise exception.NotFound("No membership found with ID %s" % member_id)

Expand All @@ -490,11 +480,16 @@ def image_member_find(context, image_id, member, session=None):
try:
# Note lack of permissions check; this function is called from
# RequestContext.is_image_visible(), so avoid recursive calls
return session.query(models.ImageMember).\
query = session.query(models.ImageMember).\
options(joinedload(models.ImageMember.image)).\
filter_by(image_id=image_id).\
filter_by(member=member).\
one()
filter_by(member=member)

if not can_show_deleted(context):
query = query.filter_by(deleted=False)

return query.one()

except exc.NoResultFound:
raise exception.NotFound("No membership found for image %s member %s" %
(image_id, member))
Expand All @@ -515,9 +510,11 @@ def image_member_get_memberships(context, member, marker=None, limit=None,
session = get_session()
query = session.query(models.ImageMember).\
options(joinedload(models.ImageMember.image)).\
filter_by(deleted=_deleted(context)).\
filter_by(member=member)

if not can_show_deleted(context):
query = query.filter_by(deleted=False)

sort_dir_func = {
'asc': asc,
'desc': desc,
Expand Down Expand Up @@ -551,7 +548,7 @@ def image_member_get_memberships(context, member, marker=None, limit=None,


# pylint: disable-msg=C0111
def _deleted(context):
def can_show_deleted(context):
"""
Calculates whether to include deleted objects based on context.
Currently just looks for a flag called deleted in the context dict.
Expand Down
16 changes: 15 additions & 1 deletion glance/registry/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@
import routes
from webob import exc

from glance.common import wsgi
from glance.common import exception
from glance.common import utils
from glance.common import wsgi
from glance.registry.db import api as db_api


Expand Down Expand Up @@ -147,8 +148,14 @@ def _get_filters(self, req):
if req.context.is_admin:
# Only admin gets to look for non-public images
filters['is_public'] = self._get_is_public(req)
# The same for deleted
filters['deleted'] = self._parse_deleted_filter(req)
else:
filters['is_public'] = True
# NOTE(jkoelker): This is technically unnecessary since the db
# api will force deleted=False if its not an
# admin context. But explicit > implicit.
filters['deleted'] = False
for param in req.str_params:
if param in SUPPORTED_FILTERS:
filters[param] = req.str_params.get(param)
Expand Down Expand Up @@ -240,6 +247,13 @@ def _get_is_public(self, req):
raise exc.HTTPBadRequest(_("is_public must be None, True, "
"or False"))

def _parse_deleted_filter(self, req):
"""Parse deleted into something usable."""
deleted = req.str_params.get('deleted', False)
if not deleted:
return False
return utils.bool_from_string(deleted)

def show(self, req, id):
"""Return data about the given image id."""
try:
Expand Down
18 changes: 18 additions & 0 deletions glance/store/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import logging
import optparse
import os
import time
import urlparse

from glance import registry
Expand Down Expand Up @@ -168,5 +169,22 @@ def schedule_delete_from_backend(uri, options, context, image_id, **kwargs):
msg = _("Failed to delete image from store (%(uri)s).") % locals()
logger.error(msg)

datadir = config.get_option(options, 'scrubber_datadir')
scrub_time = config.get_option(options, 'scrub_time', type='int',
default=0)
delete_time = time.time() + scrub_time
file_path = os.path.join(datadir, str(image_id))
utils.safe_mkdirs(datadir)

if os.path.exists(file_path):
msg = _("Image id %(image_id)s already queued for delete") % {
'image_id': image_id}
raise exception.Duplicate(msg)

with open(file_path, 'w') as f:
f.write('\n'.join([uri, str(int(delete_time))]))
os.chmod(file_path, 0600)
os.utime(file_path, (delete_time, delete_time))

registry.update_image_metadata(options, context, image_id,
{'status': 'pending_delete'})
Loading

0 comments on commit 27b3df2

Please sign in to comment.