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:
parent
c94e0070a0
commit
22ab3e0f87
@ -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."
|
||||
|
@ -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})
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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 ® or a directory in UNIX ®. 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 %}
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user