Merge "Add ability to configure read access of container"
This commit is contained in:
commit
a575d5ea05
@ -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