Add ability to configure read access of container

Allow the user to set the container access to private or public.

For public containers, the public URL for accessing it is also provided
to the user.

List containers does not include the metadata of the container. Used ajax
to load the container metadata in the Container table to be able to render
the Container table immediately.

Change-Id: If1e848ad49f522eab8f1b264d54611615481848c
Implements: blueprint swift-container-public-access
This commit is contained in:
Lin Hua Cheng 2013-09-12 15:06:26 -07:00
parent c94e0070a0
commit 22ab3e0f87
10 changed files with 240 additions and 15 deletions

View File

@ -843,8 +843,8 @@ class DataTableMetaclass(type):
# If the table is in a ResourceBrowser, the column number must meet
# these limits because of the width of the browser.
if opts.browser_table == "navigation" and len(columns) > 1:
raise ValueError("You can only assign one column to %s."
if opts.browser_table == "navigation" and len(columns) > 3:
raise ValueError("You can only assign three column to %s."
% class_name)
if opts.browser_table == "content" and len(columns) > 2:
raise ValueError("You can only assign two columns to %s."

View File

@ -19,6 +19,7 @@
# under the License.
import logging
import urllib
import swiftclient
@ -34,6 +35,9 @@ from openstack_dashboard.openstack.common import timeutils
LOG = logging.getLogger(__name__)
FOLDER_DELIMITER = "/"
# Swift ACL
GLOBAL_READ_ACL = ".r:*"
LIST_CONTENTS_ACL = ".rlistings"
class Container(base.APIDictWrapper):
@ -90,6 +94,19 @@ def _objectify(items, container_name):
return objects
def _metadata_to_header(metadata):
headers = {}
public = metadata.get('is_public')
if public is True:
public_container_acls = [GLOBAL_READ_ACL, LIST_CONTENTS_ACL]
headers['x-container-read'] = ",".join(public_container_acls)
elif public is False:
headers['x-container-read'] = ""
return headers
def swift_api(request):
endpoint = base.url_for(request, 'object-store')
cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None)
@ -139,7 +156,15 @@ def swift_get_container(request, container_name, with_data=True):
data = None
headers = swift_api(request).head_container(container_name)
timestamp = None
is_public = False
public_url = None
try:
is_public = GLOBAL_READ_ACL in headers.get('x-container-read', '')
if is_public:
swift_endpoint = base.url_for(request,
'object-store',
endpoint_type='publicURL')
public_url = swift_endpoint + '/' + urllib.quote(container_name)
ts_float = float(headers.get('x-timestamp'))
timestamp = timeutils.iso8601_from_timestamp(ts_float)
except Exception:
@ -150,14 +175,23 @@ def swift_get_container(request, container_name, with_data=True):
'container_bytes_used': headers.get('x-container-bytes-used'),
'timestamp': timestamp,
'data': data,
'is_public': is_public,
'public_url': public_url,
}
return Container(container_info)
def swift_create_container(request, name):
def swift_create_container(request, name, metadata=({})):
if swift_container_exists(request, name):
raise exceptions.AlreadyExists(name, 'container')
swift_api(request).put_container(name)
headers = _metadata_to_header(metadata)
swift_api(request).put_container(name, headers=headers)
return Container({'name': name})
def swift_update_container(request, name, metadata=({})):
headers = _metadata_to_header(metadata)
swift_api(request).post_container(name, headers=headers)
return Container({'name': name})

View File

@ -37,18 +37,30 @@ no_slash_validator = validators.RegexValidator(r'^(?u)[^/]+$',
class CreateContainer(forms.SelfHandlingForm):
ACCESS_CHOICES = (
("private", _("Private")),
("public", _("Public")),
)
parent = forms.CharField(max_length=255,
required=False,
widget=forms.HiddenInput)
name = forms.CharField(max_length=255,
label=_("Container Name"),
validators=[no_slash_validator])
access = forms.ChoiceField(label=_("Container Access"),
required=True,
choices=ACCESS_CHOICES)
def handle(self, request, data):
try:
if not data['parent']:
is_public = data["access"] == "public"
metadata = ({'is_public': is_public})
# Create a container
api.swift.swift_create_container(request, data["name"])
api.swift.swift_create_container(request,
data["name"],
metadata=metadata)
messages.success(request, _("Container created successfully."))
else:
# Create a pseudo-folder

View File

@ -13,18 +13,28 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from django.core.urlresolvers import reverse # noqa
from django.template.defaultfilters import filesizeformat # noqa
from django import shortcuts
from django import template
from django.template import defaultfilters as filters
from django.utils import http
from django.utils import safestring
from django.utils.translation import ugettext_lazy as _ # noqa
from horizon import exceptions
from horizon import messages
from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.api import swift
LOG = logging.getLogger(__name__)
LOADING_IMAGE = '<img src="/static/dashboard/img/loading.gif" />'
def wrap_delimiter(name):
if name and not name.endswith(swift.FOLDER_DELIMITER):
return name + swift.FOLDER_DELIMITER
@ -43,6 +53,58 @@ class ViewContainer(tables.LinkAction):
return reverse(self.url, args=args)
class MakePublicContainer(tables.Action):
name = "make_public"
verbose_name = _("Make Public")
classes = ("btn-edit", )
def allowed(self, request, container):
# Container metadata have not been loaded
if not hasattr(container, 'is_public'):
return False
return not container.is_public
def single(self, table, request, obj_id):
try:
api.swift.swift_update_container(request,
obj_id,
metadata=({'is_public': True}))
LOG.info('Updating container "%s" access to public.' % obj_id)
messages.success(request,
_('Successfully updated container access to '
'public.'))
except Exception:
exceptions.handle(request,
_('Unable to update container access.'))
return shortcuts.redirect('horizon:project:containers:index')
class MakePrivateContainer(tables.Action):
name = "make_private"
verbose_name = _("Make Private")
classes = ("btn-edit", )
def allowed(self, request, container):
# Container metadata have not been loaded
if not hasattr(container, 'is_public'):
return False
return container.is_public
def single(self, table, request, obj_id):
try:
api.swift.swift_update_container(request,
obj_id,
metadata=({'is_public': False}))
LOG.info('Updating container "%s" access to private.' % obj_id)
messages.success(request,
_('Successfully updated container access to '
'private.'))
except Exception:
exceptions.handle(request,
_('Unable to update container access.'))
return shortcuts.redirect('horizon:project:containers:index')
class DeleteContainer(tables.DeleteAction):
data_type_singular = _("Container")
data_type_plural = _("Containers")
@ -141,7 +203,7 @@ class UploadObject(tables.LinkAction):
def get_size_used(container):
return filesizeformat(container.bytes)
return filters.filesizeformat(container.bytes)
def get_container_link(container):
@ -149,16 +211,53 @@ def get_container_link(container):
args=(http.urlquote(wrap_delimiter(container.name)),))
class ContainerAjaxUpdateRow(tables.Row):
ajax = True
def get_data(self, request, container_name):
container = api.swift.swift_get_container(request, container_name)
return container
def get_metadata(container):
# If the metadata has not been loading, display a loading image
if not hasattr(container, 'is_public'):
return safestring.mark_safe(LOADING_IMAGE)
template_name = 'project/containers/_container_metadata.html'
context = {"container": container}
return template.loader.render_to_string(template_name, context)
def get_metadata_loaded(container):
# Determine if metadata has been loaded if the attribute is already set.
return hasattr(container, 'is_public') and container.is_public is not None
class ContainersTable(tables.DataTable):
METADATA_LOADED_CHOICES = (
(False, None),
(True, True),
)
name = tables.Column("name",
link=get_container_link,
verbose_name=_("Container Name"))
metadata = tables.Column(get_metadata,
verbose_name=_("Container Details"),
classes=('nowrap-col', ),)
metadata_loaded = tables.Column(get_metadata_loaded,
verbose_name=_("Metadata Loaded"),
status=True,
status_choices=METADATA_LOADED_CHOICES,
hidden=True)
class Meta:
name = "containers"
verbose_name = _("Containers")
row_class = ContainerAjaxUpdateRow
status_columns = ['metadata_loaded', ]
table_actions = (CreateContainer,)
row_actions = (ViewContainer, DeleteContainer,)
row_actions = (ViewContainer, MakePublicContainer,
MakePrivateContainer, DeleteContainer,)
browser_table = "navigation"
footer = False
@ -266,7 +365,7 @@ def sanitize_name(name):
def get_size(obj):
if obj.bytes:
return filesizeformat(obj.bytes)
return filters.filesizeformat(obj.bytes)
def get_link_subfolder(subfolder):

View File

@ -9,6 +9,14 @@
<dl>
<dt>{% trans "Container Name" %}</dt>
<dd>{{ container.name }}</dd>
<dt>{% trans "Container Access" %}</dt>
{% if container.public_url %}
<dd>{% trans "Public" %}</dd>
<dt>{% trans "Public URL" %}</dt>
<dd>{{ container.public_url }}</dd>
{% else %}
<dd>{% trans "Private" %}</dd>
{% endif %}
<dt>{% trans "Object Count" %}</dt>
<dd>{{ container.container_object_count }}</dd>
<dt>{% trans "Size" %}</dt>

View File

@ -0,0 +1,12 @@
{% load i18n %}
<ul>
<li>{% trans "Object Count: " %}{{ container.container_object_count }}</li>
<li>{% trans "Size: " %}: {{ container.container_bytes_used|filesizeformat }}</li>
<li>{% trans "Access: " %}
{% if container.public_url %}
<a href="{{ container.public_url }}">{% trans "Public" %}</a>
{% else %}
{% trans "Private" %}
{% endif %}
</li>
</ul>

View File

@ -16,6 +16,7 @@
<div class="right">
<h3>{% trans "Description" %}:</h3>
<p>{% trans "A container is a storage compartment for your data and provides a way for you to organize your data. You can think of a container as a folder in Windows &reg; or a directory in UNIX &reg;. The primary difference between a container and these other file system concepts is that containers cannot be nested. You can, however, create an unlimited number of containers within your account. Data must be stored in a container so you must have at least one container defined in your account prior to uploading data." %}</p>
<p>{% trans "Note: A Public Container will allow anyone with the Public URL to gain access to your objects in the container." %}</p>
</div>
{% endblock %}

View File

@ -98,10 +98,12 @@ class SwiftTests(test.TestCase):
for container in self.containers.list():
self.mox.ResetAll() # mandatory in a for loop
api.swift.swift_create_container(IsA(http.HttpRequest),
container.name)
container.name,
metadata=({'is_public': False}))
self.mox.ReplayAll()
formData = {'name': container.name,
'access': "private",
'method': forms.CreateContainer.__name__}
res = self.client.post(
reverse('horizon:project:containers:create'), formData)
@ -110,6 +112,36 @@ class SwiftTests(test.TestCase):
url = reverse('horizon:project:containers:index', args=args)
self.assertRedirectsNoFollow(res, url)
@test.create_stubs({api.swift: ('swift_update_container', )})
def test_update_container_to_public(self):
container = self.containers.get(name=u"container_one%\u6346")
api.swift.swift_update_container(IsA(http.HttpRequest),
container.name,
metadata=({'is_public': True}))
self.mox.ReplayAll()
action_string = u"containers__make_public__%s" % container.name
form_data = {"action": action_string}
req = self.factory.post(CONTAINER_INDEX_URL, form_data)
table = tables.ContainersTable(req, self.containers.list())
handled = table.maybe_handle()
self.assertEqual(handled['location'], CONTAINER_INDEX_URL)
@test.create_stubs({api.swift: ('swift_update_container', )})
def test_update_container_to_private(self):
container = self.containers.get(name=u"container_two\u6346")
api.swift.swift_update_container(IsA(http.HttpRequest),
container.name,
metadata=({'is_public': False}))
self.mox.ReplayAll()
action_string = u"containers__make_private__%s" % container.name
form_data = {"action": action_string}
req = self.factory.post(CONTAINER_INDEX_URL, form_data)
table = tables.ContainersTable(req, self.containers.list())
handled = table.maybe_handle()
self.assertEqual(handled['location'], CONTAINER_INDEX_URL)
@test.create_stubs({api.swift: ('swift_get_containers',
'swift_get_objects')})
def test_index_container_selected(self):

View File

@ -67,24 +67,45 @@ class SwiftApiTests(test.APITestCase):
self.assertIsNone(cont.data)
def test_swift_create_duplicate_container(self):
metadata = {'is_public': False}
container = self.containers.first()
headers = api.swift._metadata_to_header(metadata=(metadata))
swift_api = self.stub_swiftclient(expected_calls=2)
# Check for existence, then create
exc = self.exceptions.swift
swift_api.head_container(container.name).AndRaise(exc)
swift_api.put_container(container.name).AndReturn(container)
swift_api.put_container(container.name, headers=headers) \
.AndReturn(container)
self.mox.ReplayAll()
# Verification handled by mox, no assertions needed.
api.swift.swift_create_container(self.request, container.name)
api.swift.swift_create_container(self.request,
container.name,
metadata=(metadata))
def test_swift_create_container(self):
metadata = {'is_public': True}
container = self.containers.first()
swift_api = self.stub_swiftclient()
swift_api.head_container(container.name).AndReturn(container)
self.mox.ReplayAll()
# Verification handled by mox, no assertions needed.
with self.assertRaises(exceptions.AlreadyExists):
api.swift.swift_create_container(self.request, container.name)
api.swift.swift_create_container(self.request,
container.name,
metadata=(metadata))
def test_swift_update_container(self):
metadata = {'is_public': True}
container = self.containers.first()
swift_api = self.stub_swiftclient()
headers = api.swift._metadata_to_header(metadata=(metadata))
swift_api.post_container(container.name, headers=headers)\
.AndReturn(container)
self.mox.ReplayAll()
# Verification handled by mox, no assertions needed.
api.swift.swift_update_container(self.request,
container.name,
metadata=(metadata))
def test_swift_get_objects(self):
container = self.containers.first()

View File

@ -26,12 +26,18 @@ def data(TEST):
container_dict_1 = {"name": u"container_one%\u6346",
"container_object_count": 2,
"container_bytes_used": 256,
"timestamp": timeutils.isotime()}
"timestamp": timeutils.isotime(),
"is_public": False,
"public_url": ""}
container_1 = swift.Container(container_dict_1)
container_dict_2 = {"name": u"container_two\u6346",
"container_object_count": 4,
"container_bytes_used": 1024,
"timestamp": timeutils.isotime()}
"timestamp": timeutils.isotime(),
"is_public": True,
"public_url":
"http://public.swift.example.com:8080/" +
"v1/project_id/container_two\u6346"}
container_2 = swift.Container(container_dict_2)
TEST.containers.add(container_1, container_2)