Support last modified on listing containers

For now, last modified timestamp is supported only on
object listing. (i.e. GET container)

For example:

GET container with json format results in like as:

[{"hash": "d41d8cd98f00b204e9800998ecf8427e", "last_modified":
"2015-06-10T04:58:23.460230", "bytes": 0, "name": "object",
"content_type": "application/octet-stream"}]

However, container listing (i.e. GET account) shows just a dict
consists of ("name", "bytes", "name") for each container.

For example:

GET accounts with json format result in like as:

[{"count": 0, "bytes": 0, "name": "container"}]

This patch is for supporting last_modified key in the container
listing results as well as object listing like as:

[{"count": 0, "bytes": 0, "name": "container", "last_modified":
"2015-06-10T04:58:23.460230"}]

This patch is changing just output for listing. The original
timestamp to show the last modified is already in container table
of account.db as a "put_timestamp" column.

Note that this patch *DOESN'T* change the put_timestamp semantics.
i.e. the last_modified timestamp will be changed only at both PUT
container and POST container.
(PUT object doesn't affect the timestamp)

Note that the tuple format of returning value from
swift.account.backend.AccountBroker.list_containers is now
(name, object_count, bytes_used, put_timestamp, 0)

* put_timestamp is added *

Original discussion was in working session at Vancouver Summit.
Etherpads are around here:

https://etherpad.openstack.org/p/liberty-swift-contributors-meetup
https://etherpad.openstack.org/p/liberty-container-listing-update

DocImpact

Change-Id: Iba0503916f1481a20c59ae9136436f40183e4c5b
This commit is contained in:
Kota Tsuyuzaki 2014-12-15 20:45:41 -08:00 committed by Alistair Coles
parent 436374e66c
commit 652276fea6
10 changed files with 136 additions and 51 deletions

View File

@ -2,11 +2,13 @@
{
"count": 0,
"bytes": 0,
"name": "janeausten"
"name": "janeausten",
"last_modified": "2013-11-19T20:08:13.283452"
},
{
"count": 1,
"bytes": 14,
"name": "marktwain"
"name": "marktwain",
"last_modified": "2016-04-29T16:23:50.460230"
}
]

View File

@ -4,10 +4,12 @@
<name>janeausten</name>
<count>0</count>
<bytes>0</bytes>
<last_modified>2013-11-19T20:08:13.283452</last_modified>
</container>
<container>
<name>marktwain</name>
<count>1</count>
<bytes>14</bytes>
<last_modified>2016-04-29T16:23:50.460230</last_modified>
</container>
</account>

View File

@ -379,7 +379,8 @@ class AccountBroker(DatabaseBroker):
:param delimiter: delimiter for query
:param reverse: reverse the result order.
:returns: list of tuples of (name, object_count, bytes_used, 0)
:returns: list of tuples of (name, object_count, bytes_used,
put_timestamp, 0)
"""
delim_force_gte = False
(marker, end_marker, prefix, delimiter) = utf8encode(
@ -397,7 +398,7 @@ class AccountBroker(DatabaseBroker):
results = []
while len(results) < limit:
query = """
SELECT name, object_count, bytes_used, 0
SELECT name, object_count, bytes_used, put_timestamp, 0
FROM container
WHERE """
query_args = []
@ -459,7 +460,7 @@ class AccountBroker(DatabaseBroker):
delim_force_gte = True
dir_name = name[:end + 1]
if dir_name != orig_marker:
results.append([dir_name, 0, 0, 1])
results.append([dir_name, 0, 0, '0', 1])
curs.close()
break
results.append(row)

View File

@ -268,7 +268,7 @@ class AccountReaper(Daemon):
if not containers:
break
try:
for (container, _junk, _junk, _junk) in containers:
for (container, _junk, _junk, _junk, _junk) in containers:
this_shard = int(md5(container).hexdigest(), 16) % \
len(nodes)
if container_shard not in (this_shard, None):

View File

@ -81,24 +81,30 @@ def account_listing_response(account, req, response_content_type, broker=None,
prefix, delimiter, reverse)
if response_content_type == 'application/json':
data = []
for (name, object_count, bytes_used, is_subdir) in account_list:
for (name, object_count, bytes_used, put_timestamp, is_subdir) \
in account_list:
if is_subdir:
data.append({'subdir': name})
else:
data.append({'name': name, 'count': object_count,
'bytes': bytes_used})
data.append(
{'name': name, 'count': object_count,
'bytes': bytes_used,
'last_modified': Timestamp(put_timestamp).isoformat})
account_list = json.dumps(data)
elif response_content_type.endswith('/xml'):
output_list = ['<?xml version="1.0" encoding="UTF-8"?>',
'<account name=%s>' % saxutils.quoteattr(account)]
for (name, object_count, bytes_used, is_subdir) in account_list:
for (name, object_count, bytes_used, put_timestamp, is_subdir) \
in account_list:
if is_subdir:
output_list.append(
'<subdir name=%s />' % saxutils.quoteattr(name))
else:
item = '<container><name>%s</name><count>%s</count>' \
'<bytes>%s</bytes></container>' % \
(saxutils.escape(name), object_count, bytes_used)
'<bytes>%s</bytes><last_modified>%s</last_modified>' \
'</container>' % \
(saxutils.escape(name), object_count,
bytes_used, Timestamp(put_timestamp).isoformat)
output_list.append(item)
output_list.append('</account>')
account_list = '\n'.join(output_list)

View File

@ -439,7 +439,7 @@ class Account(Base):
tree = minidom.parseString(self.conn.response.read())
for x in tree.getElementsByTagName('container'):
cont = {}
for key in ['name', 'count', 'bytes']:
for key in ['name', 'count', 'bytes', 'last_modified']:
cont[key] = x.getElementsByTagName(key)[0].\
childNodes[0].nodeValue
conts.append(cont)

View File

@ -28,6 +28,7 @@ from copy import deepcopy
import eventlet
from unittest2 import SkipTest
from swift.common.http import is_success, is_client_error
from email.utils import parsedate
from test.functional import normalized_urls, load_constraint, cluster_info
from test.functional import check_response, retry
@ -299,6 +300,33 @@ class TestAccount(Base):
results = [r for r in results if r in expected]
self.assertEqual(expected, results)
def testContainerListingLastModified(self):
expected = {}
for container in self.env.containers:
res = container.info()
expected[container.name] = time.mktime(
parsedate(res['last_modified']))
for format_type in ['json', 'xml']:
actual = {}
containers = self.env.account.containers(
parms={'format': format_type})
if isinstance(containers[0], dict):
for container in containers:
self.assertIn('name', container) # sanity
self.assertIn('last_modified', container) # sanity
# ceil by hand (wants easier way!)
datetime_str, micro_sec_str = \
container['last_modified'].split('.')
timestamp = time.mktime(
time.strptime(datetime_str,
"%Y-%m-%dT%H:%M:%S"))
if int(micro_sec_str):
timestamp += 1
actual[container['name']] = timestamp
self.assertEqual(expected, actual)
def testInvalidAuthToken(self):
hdrs = {'X-Auth-Token': 'bogus_auth_token'}
self.assertRaises(ResponseError, self.env.account.info, hdrs=hdrs)

View File

@ -1225,18 +1225,19 @@ class TestAccountBrokerBeforeSPI(TestAccountBroker):
'from container table!')
# manually insert an existing row to avoid migration
timestamp = Timestamp(time()).internal
with broker.get() as conn:
conn.execute('''
INSERT INTO container (name, put_timestamp,
delete_timestamp, object_count, bytes_used,
deleted)
VALUES (?, ?, ?, ?, ?, ?)
''', ('test_name', Timestamp(time()).internal, 0, 1, 2, 0))
''', ('test_name', timestamp, 0, 1, 2, 0))
conn.commit()
# make sure we can iter containers without the migration
for c in broker.list_containers_iter(1, None, None, None, None):
self.assertEqual(c, ('test_name', 1, 2, 0))
self.assertEqual(c, ('test_name', 1, 2, timestamp, 0))
# stats table is mysteriously empty...
stats = broker.get_policy_stats()
@ -1363,6 +1364,7 @@ class TestAccountBrokerBeforeSPI(TestAccountBroker):
new_broker = AccountBroker(os.path.join(tempdir, 'new_account.db'),
account='a')
new_broker.initialize(next(ts).internal)
timestamp = next(ts).internal
# manually insert an existing row to avoid migration for old database
with old_broker.get() as conn:
@ -1371,7 +1373,7 @@ class TestAccountBrokerBeforeSPI(TestAccountBroker):
delete_timestamp, object_count, bytes_used,
deleted)
VALUES (?, ?, ?, ?, ?, ?)
''', ('test_name', next(ts).internal, 0, 1, 2, 0))
''', ('test_name', timestamp, 0, 1, 2, 0))
conn.commit()
# get replication info and rows form old database
@ -1384,7 +1386,7 @@ class TestAccountBrokerBeforeSPI(TestAccountBroker):
# make sure "test_name" container in new database
self.assertEqual(new_broker.get_info()['container_count'], 1)
for c in new_broker.list_containers_iter(1, None, None, None, None):
self.assertEqual(c, ('test_name', 1, 2, 0))
self.assertEqual(c, ('test_name', 1, 2, timestamp, 0))
# full migration successful
with new_broker.get() as conn:

View File

@ -87,7 +87,7 @@ class FakeAccountBroker(object):
def list_containers_iter(self, *args):
for cont in self.containers:
yield cont, None, None, None
yield cont, None, None, None, None
def is_status_deleted(self):
return True
@ -749,7 +749,7 @@ class TestReaper(unittest.TestCase):
if container in self.containers_yielded:
continue
yield container, None, None, None
yield container, None, None, None, None
self.containers_yielded.append(container)
def fake_reap_container(self, account, account_partition,

View File

@ -34,7 +34,7 @@ from swift.common.swob import (Request, WsgiBytesIO, HTTPNoContent)
from swift.common import constraints
from swift.account.server import AccountController
from swift.common.utils import (normalize_timestamp, replication, public,
mkdirs, storage_directory)
mkdirs, storage_directory, Timestamp)
from swift.common.request_helpers import get_sys_meta_prefix
from test.unit import patch_policies, debug_logger
from swift.common.storage_policy import StoragePolicy, POLICIES
@ -847,18 +847,21 @@ class TestAccountController(unittest.TestCase):
self.assertEqual(resp.charset, 'utf-8')
def test_GET_with_containers_json(self):
put_timestamps = {}
req = Request.blank('/sda1/p/a', environ={'REQUEST_METHOD': 'PUT',
'HTTP_X_TIMESTAMP': '0'})
req.get_response(self.controller)
put_timestamps['c1'] = normalize_timestamp(1)
req = Request.blank('/sda1/p/a/c1', environ={'REQUEST_METHOD': 'PUT'},
headers={'X-Put-Timestamp': '1',
headers={'X-Put-Timestamp': put_timestamps['c1'],
'X-Delete-Timestamp': '0',
'X-Object-Count': '0',
'X-Bytes-Used': '0',
'X-Timestamp': normalize_timestamp(0)})
req.get_response(self.controller)
put_timestamps['c2'] = normalize_timestamp(2)
req = Request.blank('/sda1/p/a/c2', environ={'REQUEST_METHOD': 'PUT'},
headers={'X-Put-Timestamp': '2',
headers={'X-Put-Timestamp': put_timestamps['c2'],
'X-Delete-Timestamp': '0',
'X-Object-Count': '0',
'X-Bytes-Used': '0',
@ -868,18 +871,23 @@ class TestAccountController(unittest.TestCase):
environ={'REQUEST_METHOD': 'GET'})
resp = req.get_response(self.controller)
self.assertEqual(resp.status_int, 200)
self.assertEqual(json.loads(resp.body),
[{'count': 0, 'bytes': 0, 'name': 'c1'},
{'count': 0, 'bytes': 0, 'name': 'c2'}])
self.assertEqual(
json.loads(resp.body),
[{'count': 0, 'bytes': 0, 'name': 'c1',
'last_modified': Timestamp(put_timestamps['c1']).isoformat},
{'count': 0, 'bytes': 0, 'name': 'c2',
'last_modified': Timestamp(put_timestamps['c2']).isoformat}])
put_timestamps['c1'] = normalize_timestamp(3)
req = Request.blank('/sda1/p/a/c1', environ={'REQUEST_METHOD': 'PUT'},
headers={'X-Put-Timestamp': '1',
headers={'X-Put-Timestamp': put_timestamps['c1'],
'X-Delete-Timestamp': '0',
'X-Object-Count': '1',
'X-Bytes-Used': '2',
'X-Timestamp': normalize_timestamp(0)})
req.get_response(self.controller)
put_timestamps['c2'] = normalize_timestamp(4)
req = Request.blank('/sda1/p/a/c2', environ={'REQUEST_METHOD': 'PUT'},
headers={'X-Put-Timestamp': '2',
headers={'X-Put-Timestamp': put_timestamps['c2'],
'X-Delete-Timestamp': '0',
'X-Object-Count': '3',
'X-Bytes-Used': '4',
@ -889,25 +897,31 @@ class TestAccountController(unittest.TestCase):
environ={'REQUEST_METHOD': 'GET'})
resp = req.get_response(self.controller)
self.assertEqual(resp.status_int, 200)
self.assertEqual(json.loads(resp.body),
[{'count': 1, 'bytes': 2, 'name': 'c1'},
{'count': 3, 'bytes': 4, 'name': 'c2'}])
self.assertEqual(
json.loads(resp.body),
[{'count': 1, 'bytes': 2, 'name': 'c1',
'last_modified': Timestamp(put_timestamps['c1']).isoformat},
{'count': 3, 'bytes': 4, 'name': 'c2',
'last_modified': Timestamp(put_timestamps['c2']).isoformat}])
self.assertEqual(resp.content_type, 'application/json')
self.assertEqual(resp.charset, 'utf-8')
def test_GET_with_containers_xml(self):
put_timestamps = {}
req = Request.blank('/sda1/p/a', environ={'REQUEST_METHOD': 'PUT',
'HTTP_X_TIMESTAMP': '0'})
req.get_response(self.controller)
put_timestamps['c1'] = normalize_timestamp(1)
req = Request.blank('/sda1/p/a/c1', environ={'REQUEST_METHOD': 'PUT'},
headers={'X-Put-Timestamp': '1',
headers={'X-Put-Timestamp': put_timestamps['c1'],
'X-Delete-Timestamp': '0',
'X-Object-Count': '0',
'X-Bytes-Used': '0',
'X-Timestamp': normalize_timestamp(0)})
req.get_response(self.controller)
put_timestamps['c2'] = normalize_timestamp(2)
req = Request.blank('/sda1/p/a/c2', environ={'REQUEST_METHOD': 'PUT'},
headers={'X-Put-Timestamp': '2',
headers={'X-Put-Timestamp': put_timestamps['c2'],
'X-Delete-Timestamp': '0',
'X-Object-Count': '0',
'X-Bytes-Used': '0',
@ -926,24 +940,30 @@ class TestAccountController(unittest.TestCase):
self.assertEqual(listing[0].nodeName, 'container')
container = [n for n in listing[0].childNodes if n.nodeName != '#text']
self.assertEqual(sorted([n.nodeName for n in container]),
['bytes', 'count', 'name'])
['bytes', 'count', 'last_modified', 'name'])
node = [n for n in container if n.nodeName == 'name'][0]
self.assertEqual(node.firstChild.nodeValue, 'c1')
node = [n for n in container if n.nodeName == 'count'][0]
self.assertEqual(node.firstChild.nodeValue, '0')
node = [n for n in container if n.nodeName == 'bytes'][0]
self.assertEqual(node.firstChild.nodeValue, '0')
node = [n for n in container if n.nodeName == 'last_modified'][0]
self.assertEqual(node.firstChild.nodeValue,
Timestamp(put_timestamps['c1']).isoformat)
self.assertEqual(listing[-1].nodeName, 'container')
container = \
[n for n in listing[-1].childNodes if n.nodeName != '#text']
self.assertEqual(sorted([n.nodeName for n in container]),
['bytes', 'count', 'name'])
['bytes', 'count', 'last_modified', 'name'])
node = [n for n in container if n.nodeName == 'name'][0]
self.assertEqual(node.firstChild.nodeValue, 'c2')
node = [n for n in container if n.nodeName == 'count'][0]
self.assertEqual(node.firstChild.nodeValue, '0')
node = [n for n in container if n.nodeName == 'bytes'][0]
self.assertEqual(node.firstChild.nodeValue, '0')
node = [n for n in container if n.nodeName == 'last_modified'][0]
self.assertEqual(node.firstChild.nodeValue,
Timestamp(put_timestamps['c2']).isoformat)
req = Request.blank('/sda1/p/a/c1', environ={'REQUEST_METHOD': 'PUT'},
headers={'X-Put-Timestamp': '1',
'X-Delete-Timestamp': '0',
@ -970,24 +990,30 @@ class TestAccountController(unittest.TestCase):
self.assertEqual(listing[0].nodeName, 'container')
container = [n for n in listing[0].childNodes if n.nodeName != '#text']
self.assertEqual(sorted([n.nodeName for n in container]),
['bytes', 'count', 'name'])
['bytes', 'count', 'last_modified', 'name'])
node = [n for n in container if n.nodeName == 'name'][0]
self.assertEqual(node.firstChild.nodeValue, 'c1')
node = [n for n in container if n.nodeName == 'count'][0]
self.assertEqual(node.firstChild.nodeValue, '1')
node = [n for n in container if n.nodeName == 'bytes'][0]
self.assertEqual(node.firstChild.nodeValue, '2')
node = [n for n in container if n.nodeName == 'last_modified'][0]
self.assertEqual(node.firstChild.nodeValue,
Timestamp(put_timestamps['c1']).isoformat)
self.assertEqual(listing[-1].nodeName, 'container')
container = [
n for n in listing[-1].childNodes if n.nodeName != '#text']
self.assertEqual(sorted([n.nodeName for n in container]),
['bytes', 'count', 'name'])
['bytes', 'count', 'last_modified', 'name'])
node = [n for n in container if n.nodeName == 'name'][0]
self.assertEqual(node.firstChild.nodeValue, 'c2')
node = [n for n in container if n.nodeName == 'count'][0]
self.assertEqual(node.firstChild.nodeValue, '3')
node = [n for n in container if n.nodeName == 'bytes'][0]
self.assertEqual(node.firstChild.nodeValue, '4')
node = [n for n in container if n.nodeName == 'last_modified'][0]
self.assertEqual(node.firstChild.nodeValue,
Timestamp(put_timestamps['c2']).isoformat)
self.assertEqual(resp.charset, 'utf-8')
def test_GET_xml_escapes_account_name(self):
@ -1054,15 +1080,16 @@ class TestAccountController(unittest.TestCase):
req = Request.blank('/sda1/p/a', environ={'REQUEST_METHOD': 'PUT',
'HTTP_X_TIMESTAMP': '0'})
req.get_response(self.controller)
put_timestamp = normalize_timestamp(0)
for c in range(5):
req = Request.blank(
'/sda1/p/a/c%d' % c,
environ={'REQUEST_METHOD': 'PUT'},
headers={'X-Put-Timestamp': str(c + 1),
headers={'X-Put-Timestamp': put_timestamp,
'X-Delete-Timestamp': '0',
'X-Object-Count': '2',
'X-Bytes-Used': '3',
'X-Timestamp': normalize_timestamp(0)})
'X-Timestamp': put_timestamp})
req.get_response(self.controller)
req = Request.blank('/sda1/p/a?limit=3',
environ={'REQUEST_METHOD': 'GET'})
@ -1079,31 +1106,38 @@ class TestAccountController(unittest.TestCase):
req = Request.blank('/sda1/p/a', environ={'REQUEST_METHOD': 'PUT',
'HTTP_X_TIMESTAMP': '0'})
req.get_response(self.controller)
put_timestamp = normalize_timestamp(0)
for c in range(5):
req = Request.blank(
'/sda1/p/a/c%d' % c,
environ={'REQUEST_METHOD': 'PUT'},
headers={'X-Put-Timestamp': str(c + 1),
headers={'X-Put-Timestamp': put_timestamp,
'X-Delete-Timestamp': '0',
'X-Object-Count': '2',
'X-Bytes-Used': '3',
'X-Timestamp': normalize_timestamp(0)})
'X-Timestamp': put_timestamp})
req.get_response(self.controller)
req = Request.blank('/sda1/p/a?limit=3&format=json',
environ={'REQUEST_METHOD': 'GET'})
resp = req.get_response(self.controller)
self.assertEqual(resp.status_int, 200)
self.assertEqual(json.loads(resp.body),
[{'count': 2, 'bytes': 3, 'name': 'c0'},
{'count': 2, 'bytes': 3, 'name': 'c1'},
{'count': 2, 'bytes': 3, 'name': 'c2'}])
timestamp_str = Timestamp(put_timestamp).isoformat
expected = [{'count': 2, 'bytes': 3, 'name': 'c0',
'last_modified': timestamp_str},
{'count': 2, 'bytes': 3, 'name': 'c1',
'last_modified': timestamp_str},
{'count': 2, 'bytes': 3, 'name': 'c2',
'last_modified': timestamp_str}]
self.assertEqual(json.loads(resp.body), expected)
req = Request.blank('/sda1/p/a?limit=3&marker=c2&format=json',
environ={'REQUEST_METHOD': 'GET'})
resp = req.get_response(self.controller)
self.assertEqual(resp.status_int, 200)
self.assertEqual(json.loads(resp.body),
[{'count': 2, 'bytes': 3, 'name': 'c3'},
{'count': 2, 'bytes': 3, 'name': 'c4'}])
expected = [{'count': 2, 'bytes': 3, 'name': 'c3',
'last_modified': timestamp_str},
{'count': 2, 'bytes': 3, 'name': 'c4',
'last_modified': timestamp_str}]
self.assertEqual(json.loads(resp.body), expected)
def test_GET_limit_marker_xml(self):
req = Request.blank('/sda1/p/a', environ={'REQUEST_METHOD': 'PUT',
@ -1131,24 +1165,31 @@ class TestAccountController(unittest.TestCase):
self.assertEqual(listing[0].nodeName, 'container')
container = [n for n in listing[0].childNodes if n.nodeName != '#text']
self.assertEqual(sorted([n.nodeName for n in container]),
['bytes', 'count', 'name'])
['bytes', 'count', 'last_modified', 'name'])
node = [n for n in container if n.nodeName == 'name'][0]
self.assertEqual(node.firstChild.nodeValue, 'c0')
node = [n for n in container if n.nodeName == 'count'][0]
self.assertEqual(node.firstChild.nodeValue, '2')
node = [n for n in container if n.nodeName == 'bytes'][0]
self.assertEqual(node.firstChild.nodeValue, '3')
node = [n for n in container if n.nodeName == 'last_modified'][0]
self.assertEqual(node.firstChild.nodeValue,
Timestamp('1').isoformat)
self.assertEqual(listing[-1].nodeName, 'container')
container = [
n for n in listing[-1].childNodes if n.nodeName != '#text']
self.assertEqual(sorted([n.nodeName for n in container]),
['bytes', 'count', 'name'])
['bytes', 'count', 'last_modified', 'name'])
node = [n for n in container if n.nodeName == 'name'][0]
node = [n for n in container if n.nodeName == 'name'][0]
self.assertEqual(node.firstChild.nodeValue, 'c2')
node = [n for n in container if n.nodeName == 'count'][0]
self.assertEqual(node.firstChild.nodeValue, '2')
node = [n for n in container if n.nodeName == 'bytes'][0]
self.assertEqual(node.firstChild.nodeValue, '3')
node = [n for n in container if n.nodeName == 'last_modified'][0]
self.assertEqual(node.firstChild.nodeValue,
Timestamp('3').isoformat)
req = Request.blank('/sda1/p/a?limit=3&marker=c2&format=xml',
environ={'REQUEST_METHOD': 'GET'})
resp = req.get_response(self.controller)
@ -1161,18 +1202,21 @@ class TestAccountController(unittest.TestCase):
self.assertEqual(listing[0].nodeName, 'container')
container = [n for n in listing[0].childNodes if n.nodeName != '#text']
self.assertEqual(sorted([n.nodeName for n in container]),
['bytes', 'count', 'name'])
['bytes', 'count', 'last_modified', 'name'])
node = [n for n in container if n.nodeName == 'name'][0]
self.assertEqual(node.firstChild.nodeValue, 'c3')
node = [n for n in container if n.nodeName == 'count'][0]
self.assertEqual(node.firstChild.nodeValue, '2')
node = [n for n in container if n.nodeName == 'bytes'][0]
self.assertEqual(node.firstChild.nodeValue, '3')
node = [n for n in container if n.nodeName == 'last_modified'][0]
self.assertEqual(node.firstChild.nodeValue,
Timestamp('4').isoformat)
self.assertEqual(listing[-1].nodeName, 'container')
container = [
n for n in listing[-1].childNodes if n.nodeName != '#text']
self.assertEqual(sorted([n.nodeName for n in container]),
['bytes', 'count', 'name'])
['bytes', 'count', 'last_modified', 'name'])
node = [n for n in container if n.nodeName == 'name'][0]
self.assertEqual(node.firstChild.nodeValue, 'c4')
node = [n for n in container if n.nodeName == 'count'][0]