Browse Source

Merge remote-tracking branch 'origin/master' into feature/losf

Change-Id: I5eb60d51b880a8e8f0578f870f6f237b61a29857
changes/64/686864/1
Tim Burke 4 months ago
parent
commit
bfa8e9feb5
94 changed files with 2411 additions and 1514 deletions
  1. +1
    -0
      .gitignore
  2. +21
    -2
      .zuul.yaml
  3. +5
    -0
      AUTHORS
  4. +69
    -1
      CHANGELOG
  5. +16
    -13
      api-ref/source/conf.py
  6. +9
    -2
      bandit.yaml
  7. +1
    -1
      bin/swift-orphans
  8. +1
    -0
      doc/requirements.txt
  9. +11
    -3
      doc/source/conf.py
  10. +1
    -3
      doc/source/getting_started.rst
  11. +1
    -1
      doc/source/index.rst
  12. +12
    -12
      doc/source/overview_acl.rst
  13. +27
    -24
      doc/source/overview_ring.rst
  14. +74
    -0
      releasenotes/notes/2_23_0_release-2a2d11c1934f0b61.yaml
  15. +2
    -0
      releasenotes/source/index.rst
  16. +0
    -75
      releasenotes/source/locale/ko_KR/LC_MESSAGES/releasenotes.po
  17. +6
    -0
      releasenotes/source/train.rst
  18. +7
    -3
      swift/account/backend.py
  19. +0
    -3
      swift/account/server.py
  20. +1
    -1
      swift/cli/ringbuilder.py
  21. +10
    -1
      swift/common/bufferedhttp.py
  22. +3
    -0
      swift/common/manager.py
  23. +9
    -4
      swift/common/middleware/s3api/controllers/multi_upload.py
  24. +71
    -12
      swift/common/middleware/s3api/s3api.py
  25. +8
    -10
      swift/common/middleware/s3api/s3request.py
  26. +12
    -10
      swift/common/middleware/s3api/s3token.py
  27. +2
    -0
      swift/common/middleware/s3api/subresource.py
  28. +6
    -1
      swift/common/middleware/versioned_writes.py
  29. +4
    -2
      swift/common/ring/builder.py
  30. +120
    -9
      swift/common/ring/ring.py
  31. +37
    -0
      swift/common/wsgi.py
  32. +33
    -11
      swift/container/backend.py
  33. +0
    -3
      swift/container/server.py
  34. +91
    -16
      swift/container/sharder.py
  35. +210
    -68
      swift/locale/de/LC_MESSAGES/swift.po
  36. +1
    -83
      swift/locale/en_GB/LC_MESSAGES/swift.po
  37. +1
    -76
      swift/locale/es/LC_MESSAGES/swift.po
  38. +1
    -74
      swift/locale/fr/LC_MESSAGES/swift.po
  39. +1
    -76
      swift/locale/it/LC_MESSAGES/swift.po
  40. +1
    -76
      swift/locale/ja/LC_MESSAGES/swift.po
  41. +1
    -70
      swift/locale/ko_KR/LC_MESSAGES/swift.po
  42. +1
    -71
      swift/locale/pt_BR/LC_MESSAGES/swift.po
  43. +1
    -73
      swift/locale/ru/LC_MESSAGES/swift.po
  44. +1
    -70
      swift/locale/tr_TR/LC_MESSAGES/swift.po
  45. +1
    -70
      swift/locale/zh_CN/LC_MESSAGES/swift.po
  46. +1
    -70
      swift/locale/zh_TW/LC_MESSAGES/swift.po
  47. +5
    -1
      swift/obj/diskfile.py
  48. +3
    -1
      swift/proxy/controllers/base.py
  49. +14
    -12
      swift/proxy/controllers/container.py
  50. +8
    -7
      swift/proxy/controllers/obj.py
  51. +7
    -0
      swift/proxy/server.py
  52. +14
    -2
      test/functional/__init__.py
  53. +18
    -0
      test/functional/s3api/test_bucket.py
  54. +7
    -1
      test/functional/swift_test_client.py
  55. +152
    -3
      test/functional/test_versioned_writes.py
  56. +49
    -0
      test/functional/tests.py
  57. +9
    -9
      test/probe/common.py
  58. +3
    -3
      test/probe/test_account_get_fake_responses_match.py
  59. +1
    -1
      test/probe/test_container_failures.py
  60. +30
    -29
      test/probe/test_container_merge_policy_index.py
  61. +14
    -14
      test/probe/test_container_sync.py
  62. +2
    -2
      test/probe/test_empty_device_handoff.py
  63. +3
    -3
      test/probe/test_object_async_update.py
  64. +6
    -6
      test/probe/test_object_conditional_requests.py
  65. +10
    -7
      test/probe/test_object_failures.py
  66. +11
    -11
      test/probe/test_object_handoff.py
  67. +5
    -5
      test/probe/test_object_metadata_replication.py
  68. +2
    -2
      test/probe/test_object_partpower_increase.py
  69. +19
    -13
      test/probe/test_reconstructor_rebuild.py
  70. +1
    -1
      test/probe/test_reconstructor_revert.py
  71. +97
    -34
      test/probe/test_sharder.py
  72. +7
    -7
      test/probe/test_signals.py
  73. +106
    -14
      test/unit/account/test_server.py
  74. +24
    -0
      test/unit/cli/test_ringbuilder.py
  75. +22
    -2
      test/unit/common/middleware/s3api/test_multi_upload.py
  76. +36
    -44
      test/unit/common/middleware/s3api/test_s3api.py
  77. +7
    -2
      test/unit/common/middleware/s3api/test_s3request.py
  78. +5
    -10
      test/unit/common/middleware/s3api/test_s3token.py
  79. +15
    -0
      test/unit/common/ring/test_ring.py
  80. +3
    -2
      test/unit/common/test_bufferedhttp.py
  81. +7
    -7
      test/unit/common/test_manager.py
  82. +7
    -1
      test/unit/common/test_wsgi.py
  83. +112
    -26
      test/unit/container/test_server.py
  84. +194
    -1
      test/unit/container/test_sharder.py
  85. +7
    -5
      test/unit/container/test_sync.py
  86. +22
    -11
      test/unit/helpers.py
  87. +1
    -1
      test/unit/obj/test_replicator.py
  88. +138
    -61
      test/unit/proxy/controllers/test_container.py
  89. +29
    -11
      test/unit/proxy/controllers/test_obj.py
  90. +230
    -4
      test/unit/proxy/test_server.py
  91. +12
    -12
      tools/playbooks/ceph-s3tests/run.yaml
  92. +7
    -1
      tools/playbooks/multinode_setup/make_rings.yaml
  93. +23
    -0
      tools/playbooks/saio_single_node_setup/add_s3api.yaml
  94. +15
    -25
      tox.ini

+ 1
- 0
.gitignore View File

@@ -22,3 +22,4 @@ subunit.log
test/probe/.noseids
RELEASENOTES.rst
releasenotes/notes/reno.cache
/tools/playbooks/**/*.retry

+ 21
- 2
.zuul.yaml View File

@@ -175,7 +175,6 @@
Uses tox with the ``func-ec`` environment.
It sets TMPDIR to an XFS mount point created via
tools/test-setup.sh.
branches: ^(?!stable/ocata).*$
vars:
tox_envlist: func-ec

@@ -267,8 +266,10 @@
parent: swift-dsvm-functional
description: |
Setup a Swift/Keystone environment under py3 and run Swift's func tests
(but under py2).
(also under py3).
vars:
# This tox env get run twice; once for Keystone and once for tempauth
tox_envlist: func-py3
devstack_localrc:
USE_PYTHON3: true
# explicitly clear swift's default-disabled status
@@ -292,6 +293,7 @@
pre-run:
- tools/playbooks/common/install_dependencies.yaml
- tools/playbooks/saio_single_node_setup/setup_saio.yaml
- tools/playbooks/saio_single_node_setup/add_s3api.yaml
- tools/playbooks/saio_single_node_setup/make_rings.yaml
run: tools/playbooks/ceph-s3tests/run.yaml
post-run:
@@ -381,6 +383,12 @@
vars:
previous_swift_version: origin/stable/rocky

- job:
name: swift-multinode-rolling-upgrade-stein
parent: swift-multinode-rolling-upgrade
vars:
previous_swift_version: origin/stable/stein

- job:
name: swift-tox-lower-constraints
parent: openstack-tox-lower-constraints
@@ -574,6 +582,11 @@
- ^(api-ref|doc|releasenotes)/.*$
- ^test/.*$
- ^(.gitreview|.mailmap|AUTHORS|CHANGELOG)$
- tempest-ipv6-only:
irrelevant-files:
- ^(api-ref|doc|releasenotes)/.*$
- ^test/.*$
- ^(.gitreview|.mailmap|AUTHORS|CHANGELOG)$
- grenade-py3:
irrelevant-files:
- ^(api-ref|doc|releasenotes)/.*$
@@ -635,6 +648,11 @@
- ^(api-ref|doc|releasenotes)/.*$
- ^test/.*$
- ^(.gitreview|.mailmap|AUTHORS|CHANGELOG)$
- tempest-ipv6-only:
irrelevant-files:
- ^(api-ref|doc|releasenotes)/.*$
- ^test/.*$
- ^(.gitreview|.mailmap|AUTHORS|CHANGELOG)$
- grenade-py3:
irrelevant-files:
- ^(api-ref|doc|releasenotes)/.*$
@@ -648,6 +666,7 @@
- swift-tox-func-ec-centos-7
- swift-multinode-rolling-upgrade-queens
- swift-multinode-rolling-upgrade-rocky
- swift-multinode-rolling-upgrade-stein

post:
jobs:

+ 5
- 0
AUTHORS View File

@@ -58,6 +58,7 @@ Anh Tran (anhtt@vn.fujitsu.com)
Ankur Gupta (ankur.gupta@intel.com)
Anne Gentle (anne@openstack.org)
Arnaud JOST (arnaud.jost@ovh.net)
arzhna (arzhna@gmail.com)
Atsushi Sakai (sakaia@jp.fujitsu.com)
Azhagu Selvan SP (tamizhgeek@gmail.com)
baiwenteng (baiwenteng@inspur.com)
@@ -107,6 +108,7 @@ Constantine Peresypkin (constantine.peresypk@rackspace.com)
Corey Bryant (corey.bryant@canonical.com)
Cory Wright (cory.wright@rackspace.com)
Cristian A Sanchez (cristian.a.sanchez@intel.com)
CY Chiang (cychiang@cht.com.tw)
Cyril Roelandt (cyril@redhat.com)
Dae S. Kim (dae@velatum.com)
Daisuke Morita (morita.daisuke@ntti3.com)
@@ -173,6 +175,7 @@ gecong1973 (ge.cong@zte.com.cn)
gengchc2 (geng.changcai2@zte.com.cn)
Gerard Gine (ggine@swiftstack.com)
Gerry Drudy (gerry.drudy@hpe.com)
Ghanshyam Mann (gmann@ghanshyammann.com)
Gil Vernik (gilv@il.ibm.com)
Gilles Biannic (gilles.biannic@corp.ovh.com)
Gleb Samsonov (sams-gleb@yandex.ru)
@@ -307,6 +310,7 @@ Ngo Quoc Cuong (cuongnq@vn.fujitsu.com)
Nguyen Hai (nguyentrihai93@gmail.com)
Nguyen Hung Phuong (phuongnh@vn.fujitsu.com)
Nguyen Phuong An (AnNP@vn.fujitsu.com)
Nguyen Quoc Viet (nguyenqviet98@gmail.com)
Nicolas Helgeson (nh202b@att.com)
Nicolas Trangez (ikke@nicolast.be)
Ning Zhang (ning@zmanda.com)
@@ -323,6 +327,7 @@ Paul McMillan (paul.mcmillan@nebula.com)
Pavel Kvasnička (pavel.kvasnicka@firma.seznam.cz)
Pawel Palucki (pawel.palucki@gmail.com)
Pearl Yajing Tan (pearl.y.tan@seagate.com)
pengyuesheng (pengyuesheng@gohighsec.com)
Pete Zaitcev (zaitcev@kotori.zaitcev.us)
Peter Lisák (peter.lisak@gmail.com)
Peter Portante (peter.portante@redhat.com)

+ 69
- 1
CHANGELOG View File

@@ -1,3 +1,61 @@
swift (2.23.0, OpenStack Train)

* Python 3.6 and 3.7 are now fully supported. Several py3-related
fixes are included:

* Removed a request-smuggling vector when running a mixed
py2/py3 cluster.

* Allow fallocate_reserve to be specified as a percentage.

* Fixed listings for sharded containers.

* Fixed non-ASCII account metadata handling.

* Fixed rsync output parsing.

* Fixed some title-casing of headers.

If you've been testing Swift on Python 3, upgrade at your earliest
convenience.

* Added "static symlinks", which perform some validation as they
follow redirects and include more information about their target
in container listings.

* Multi-character strings may now be used as delimiters in account
and container listings.

* Sharding improvements

* Container metadata related to sharding are now removed when no
longer needed.

* Empty container databases (such as might be created on handoffs)
now shard much more quickly.

* The proxy-server now ignores 404 responses from handoffs that have
no data when deciding on the correct response for object requests,
similar to what it already does for account and container requests.

* Static Large Object sizes in listings for versioned containers are
now more accurate.

* When refetching Static Large Object manifests, non-manifest responses
are now handled better.

* S3 API now translates 503 Service Unavailable responses to a more
S3-like response instead of raising an error.

* Improved proxy-to-backend requests to be more RFC-compliant.

* Dependency update: eventlet must be at least 0.25.0. This also
dragged forward minimum-supported versions of dnspython (1.15.0),
greenlet (0.3.2), and six (1.10.0).

* Various other minor bug fixes and improvements.


swift (2.22.0)

* Experimental support for Python 3.6 and 3.7 is now available.
@@ -73,7 +131,8 @@ swift (2.22.0)

* Various other minor bug fixes and improvements.

swift (2.21.0, OpenStack Stein release)

swift (2.21.0, OpenStack Stein)

* Change the behavior of the EC reconstructor to perform a
fragment rebuild to a handoff node when a primary peer responds
@@ -131,6 +190,7 @@ swift (2.21.0, OpenStack Stein release)

* Various other minor bug fixes and improvements.


swift (2.20.0)

* S3 API compatibility updates
@@ -237,6 +297,7 @@ swift (2.20.0)

* Various other minor bug fixes and improvements.


swift (2.19.1, rocky stable backports)

* Prevent PyKMIP's kmip_protocol logger from logging at DEBUG.
@@ -251,6 +312,7 @@ swift (2.19.1, rocky stable backports)
* Fixed a bug in how Swift uses eventlet that was exposed under high
concurrency.


swift (2.19.0, OpenStack Rocky)

* TempURLs now support IP range restrictions. Please see
@@ -341,6 +403,7 @@ swift (2.19.0, OpenStack Rocky)

* Various other minor bug fixes and improvements.


swift (2.18.0)

* Added container sharding, an operator controlled feature that
@@ -414,6 +477,7 @@ swift (2.18.0)

* Various other minor bug fixes and improvements.


swift (2.17.1, queens stable backports)

* Fix SLO delete for accounts with non-ASCII names.
@@ -424,6 +488,7 @@ swift (2.17.1, queens stable backports)
* Fixed a bug in how Swift uses eventlet that was exposed under high
concurrency.


swift (2.17.0, OpenStack Queens)

* Added symlink objects support.
@@ -616,6 +681,7 @@ swift (2.16.0)

* Various other minor bug fixes and improvements.


swift (2.15.2, pike stable backports)

* Fixed a cache invalidation issue related to GET and PUT requests to
@@ -636,6 +702,7 @@ swift (2.15.2, pike stable backports)

* Send ETag header in 206 Partial Content responses to SLO reads.


swift (2.15.1, OpenStack Pike)

* Fixed a bug introduced in 2.15.0 where the object reconstructor
@@ -3062,6 +3129,7 @@ swift (1.4.0)
* Stats uploaders now allow overrides for source_filename_pattern and
new_log_cutoff values.


----

Changelog entries for previous versions are incomplete

+ 16
- 13
api-ref/source/conf.py View File

@@ -23,6 +23,7 @@
# All configuration values have a default; values that are commented out
# serve to show the default.

import datetime
import os
from swift import __version__
import subprocess
@@ -154,20 +155,22 @@ pygments_style = 'sphinx'
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y'
git_cmd = ["git", "log", "--pretty=format:'%ad, commit %h'", "--date=local",
"-n1"]
try:
html_last_updated_fmt = subprocess.Popen(
git_cmd, stdout=subprocess.PIPE).communicate()[0]
except OSError:
warnings.warn('Cannot get last updated time from git repository. '
'Not setting "html_last_updated_fmt".')
if 'SOURCE_DATE_EPOCH' in os.environ:
now = float(os.environ.get('SOURCE_DATE_EPOCH'))
html_last_updated_fmt = datetime.datetime.utcfromtimestamp(now).isoformat()
else:
if not isinstance(html_last_updated_fmt, str):
# for py3
html_last_updated_fmt = html_last_updated_fmt.decode('ascii')


git_cmd = ["git", "log", "--pretty=format:'%ad, commit %h'", "--date=local",
"-n1"]
try:
html_last_updated_fmt = subprocess.Popen(
git_cmd, stdout=subprocess.PIPE).communicate()[0]
except OSError:
warnings.warn('Cannot get last updated time from git repository. '
'Not setting "html_last_updated_fmt".')
else:
if not isinstance(html_last_updated_fmt, str):
# for py3
html_last_updated_fmt = html_last_updated_fmt.decode('ascii')
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
# html_use_smartypants = True

+ 9
- 2
bandit.yaml View File

@@ -16,9 +16,7 @@
# B106 : hardcoded_password_funcarg
# B107 : hardcoded_password_default
# B108 : hardcoded_tmp_directory
# B109 : password_config_option_not_marked_secret
# B110 : try_except_pass
# B111 : execute_with_run_as_root_equals_true
# B112 : try_except_continue
# B201 : flask_debug_true
# B301 : pickle
@@ -42,6 +40,9 @@
# B319 : xml_bad_pulldom
# B320 : xml_bad_etree
# B321 : ftplib
# B322 : input
# B323 : unverified_context
# B325 : tempnam
# B401 : import_telnetlib
# B402 : import_ftplib
# B403 : import_pickle
@@ -54,12 +55,15 @@
# B410 : import_lxml
# B411 : import_xmlrpclib
# B412 : import_httpoxy
# B413 : import_pycrypto
# B414 : import_pycryptodome
# B501 : request_with_no_cert_validation
# B502 : ssl_with_bad_version
# B503 : ssl_with_bad_defaults
# B504 : ssl_with_no_version
# B505 : weak_cryptographic_key
# B506 : yaml_load
# B507 : ssh_no_host_key_verification
# B601 : paramiko_calls
# B602 : subprocess_popen_with_shell_equals_true
# B603 : subprocess_without_shell_equals_true
@@ -69,8 +73,11 @@
# B607 : start_process_with_partial_path
# B608 : hardcoded_sql_expressions
# B609 : linux_commands_wildcard_injection
# B610 : django_extra_used
# B611 : django_rawsql_used
# B701 : jinja2_autoescape_false
# B702 : use_of_mako_templates
# B703 : django_mark_safe

# (optional) list included test IDs here, eg '[B101, B406]':
tests: [B102, B103, B302, B306, B308, B309, B310, B401, B501, B502, B506, B601, B602, B609]

+ 1
- 1
bin/swift-orphans View File

@@ -53,7 +53,7 @@ Example (sends SIGTERM to all orphaned Swift processes older than two hours):

for root, directories, files in os.walk(options.run_dir):
for name in files:
if name.endswith('.pid'):
if name.endswith(('.pid', '.pid.d')):
pids.append(open(os.path.join(root, name)).read().strip())
pids.extend(subprocess.Popen(
['ps', '--ppid', pids[-1], '-o', 'pid', '--no-headers'],

+ 1
- 0
doc/requirements.txt View File

@@ -8,3 +8,4 @@ openstackdocstheme>=1.30.0 # Apache-2.0
reno>=1.8.0 # Apache-2.0
os-api-ref>=1.0.0 # Apache-2.0
python-keystoneclient!=2.1.0,>=2.0.0 # Apache-2.0
sphinxcontrib-svg2pdfconverter>=0.1.0 # BSD

+ 11
- 3
doc/source/conf.py View File

@@ -55,7 +55,8 @@ extensions = ['sphinx.ext.autodoc',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.ifconfig',
'openstackdocstheme']
'openstackdocstheme',
'sphinxcontrib.rsvgconverter']
todo_include_todos = True

# Add any paths that contain templates here, relative to this directory.
@@ -72,7 +73,12 @@ master_doc = 'index'

# General information about the project.
project = u'Swift'
copyright = u'%d, OpenStack Foundation' % datetime.datetime.now().year
if 'SOURCE_DATE_EPOCH' in os.environ:
now = float(os.environ.get('SOURCE_DATE_EPOCH'))
now = datetime.datetime.utcfromtimestamp(now)
else:
now = datetime.date.today()
copyright = u'%d, OpenStack Foundation' % now.year

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@@ -218,7 +224,7 @@ htmlhelp_basename = 'swiftdoc'
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
('index', 'Swift.tex', u'Swift Documentation',
('index', 'doc-swift.tex', u'Swift Documentation',
u'Swift Team', 'manual'),
]

@@ -239,6 +245,8 @@ latex_documents = [
# If false, no module index is generated.
# latex_use_modindex = True

latex_use_xindy = False

# -- Options for openstackdocstheme -------------------------------------------
repository_name = 'openstack/swift'
bug_project = 'swift'

+ 1
- 3
doc/source/getting_started.rst View File

@@ -11,14 +11,12 @@ most Linux platforms.

Swift is written in Python and has these dependencies:

* Python 2.7
* Python (2.7, 3.6, or 3.7)
* rsync 3.0
* The Python packages listed in `the requirements file <https://github.com/openstack/swift/blob/master/requirements.txt>`_
* Testing additionally requires `the test dependencies <https://github.com/openstack/swift/blob/master/test-requirements.txt>`_
* Testing requires `these distribution packages <https://github.com/openstack/swift/blob/master/bindep.txt>`_

There is no current support for Python 3.

-----------
Development
-----------

+ 1
- 1
doc/source/index.rst View File

@@ -33,7 +33,7 @@ be found on the `OpenStack wiki`_ and at http://docs.openstack.org.


.. toctree::
:maxdepth: 1
:maxdepth: 2

getting_started


+ 12
- 12
doc/source/overview_acl.rst View File

@@ -75,17 +75,17 @@ of ``.rlistings``, an error will occur if used with
============================== ================================================
Element Description
============================== ================================================
``.r:*`` Any user has access to objects. No token is
.r:* Any user has access to objects. No token is
required in the request.
``.r:<referrer>`` The referrer is granted access to objects. The
.r:<referrer> The referrer is granted access to objects. The
referrer is identified by the ``Referer``
request header in the request. No token is
required.
``.r:-<referrer>`` This syntax (with "-" prepended to the
.r:-<referrer> This syntax (with "-" prepended to the
referrer) is supported. However, it does not
deny access if another element (e.g., ``.r:*``)
grants access.
``.rlistings`` Any user can perform a HEAD or GET operation
.rlistings Any user can perform a HEAD or GET operation
on the container provided the user also has
read access on objects (e.g., also has ``.r:*``
or ``.r:<referrer>``. No token is required.
@@ -106,22 +106,22 @@ to take effect.
============================== ================================================
Element Description
============================== ================================================
``<project-id>:<user-id>`` The specified user, provided a token
<project-id>:<user-id> The specified user, provided a token
scoped to the project is included
in the request, is granted access.
Access to the container is also granted
when used in ``X-Container-Read``.
``<project-id>:*`` Any user with a role in the specified Keystone
<project-id>:\* Any user with a role in the specified Keystone
project has access. A token scoped to the
project must be included in the request.
Access to the container is also granted
when used in ``X-Container-Read``.
``*:<user-id>`` The specified user has access. A token
\*:<user-id> The specified user has access. A token
for the user (scoped to any
project) must be included in the request.
Access to the container is also granted
when used in ``X-Container-Read``.
``*:*`` Any user has access.
\*:\* Any user has access.
Access to the container is also granted
when used in ``X-Container-Read``.
The ``*:*`` element differs from the ``.r:*``
@@ -131,7 +131,7 @@ Element Description
does not require a token. In addition,
``.r:*`` does not grant access to the
container listing.
``<role_name>`` A user with the specified role *name* on the
<role_name> A user with the specified role *name* on the
project within which the container is stored is
granted access. A user token scoped to the
project must be included in the request. Access
@@ -142,7 +142,7 @@ Element Description
.. note::

Keystone project (tenant) or user *names* (i.e.,
``<project-name>:<user-name``) must no longer be
``<project-name>:<user-name>``) must no longer be
used because with the introduction
of domains in Keystone, names are not globally unique. You should
use user and project *ids* instead.
@@ -167,7 +167,7 @@ the elements described in :ref:`acl_common_elements`.
============================== ================================================
Element Description
============================== ================================================
``<user-name>`` The named user is granted access. The
<user-name> The named user is granted access. The
wildcard ("*") character is not supported.
A token from the user must be included in the
request.
@@ -407,4 +407,4 @@ admin These identities have "swift_owner" privileges. A user with


For more details, see :mod:`swift.common.middleware.tempauth`. For details
on the ACL format, see :mod:`swift.common.middleware.acl`.
on the ACL format, see :mod:`swift.common.middleware.acl`.

+ 27
- 24
doc/source/overview_ring.rst View File

@@ -82,30 +82,33 @@ List of Devices
The list of devices is known internally to the Ring class as ``devs``. Each
item in the list of devices is a dictionary with the following keys:

====== ======= ==============================================================
id integer The index into the list of devices.
zone integer The zone in which the device resides.
region integer The region in which the zone resides.
weight float The relative weight of the device in comparison to other
devices. This usually corresponds directly to the amount of
disk space the device has compared to other devices. For
instance a device with 1 terabyte of space might have a weight
of 100.0 and another device with 2 terabytes of space might
have a weight of 200.0. This weight can also be used to bring
back into balance a device that has ended up with more or less
data than desired over time. A good average weight of 100.0
allows flexibility in lowering the weight later if necessary.
ip string The IP address or hostname of the server containing the device.
port int The TCP port on which the server process listens to serve
requests for the device.
device string The on-disk name of the device on the server.
For example: ``sdb1``
meta string A general-use field for storing additional information for the
device. This information isn't used directly by the server
processes, but can be useful in debugging. For example, the
date and time of installation and hardware manufacturer could
be stored here.
====== ======= ==============================================================
.. table::
:widths: 10 10 80

====== ======= ==============================================================
id integer The index into the list of devices.
zone integer The zone in which the device resides.
region integer The region in which the zone resides.
weight float The relative weight of the device in comparison to other
devices. This usually corresponds directly to the amount of
disk space the device has compared to other devices. For
instance a device with 1 terabyte of space might have a weight
of 100.0 and another device with 2 terabytes of space might
have a weight of 200.0. This weight can also be used to bring
back into balance a device that has ended up with more or less
data than desired over time. A good average weight of 100.0
allows flexibility in lowering the weight later if necessary.
ip string The IP address or hostname of the server containing the device.
port int The TCP port on which the server process listens to serve
requests for the device.
device string The on-disk name of the device on the server.
For example: ``sdb1``
meta string A general-use field for storing additional information for the
device. This information isn't used directly by the server
processes, but can be useful in debugging. For example, the
date and time of installation and hardware manufacturer could
be stored here.
====== ======= ==============================================================

.. note::
The list of devices may contain holes, or indexes set to ``None``, for

+ 74
- 0
releasenotes/notes/2_23_0_release-2a2d11c1934f0b61.yaml View File

@@ -0,0 +1,74 @@
---
features:
- |
Python 3.6 and 3.7 are now fully supported. If you've been testing Swift
on Python 3, upgrade at your earliest convenience.

- |
Added "static symlinks", which perform some validation as they
follow redirects and include more information about their target
in container listings. For more information, see the `symlink middleware
<https://docs.openstack.org/swift/latest/middleware.html#symlink>`__
section of the documentation.

- |
Multi-character strings may now be used as delimiters in account
and container listings.

upgrade:
- |
**Dependency update**: ``eventlet`` must be at least 0.25.0. This also
dragged forward minimum-supported versions of ``dnspython`` (1.15.0),
``greenlet`` (0.3.2), and ``six`` (1.10.0).

fixes:
- |
Python 3 fixes:

* Removed a request-smuggling vector when running a mixed
py2/py3 cluster.

* Allow ``fallocate_reserve`` to be specified as a percentage.

* Fixed listings for sharded containers.

* Fixed non-ASCII account metadata handling.

* Fixed ``rsync`` output parsing.

* Fixed some title-casing of headers.

If you've been testing Swift on Python 3, upgrade at your earliest
convenience.

- |
Sharding improvements

* Container metadata related to sharding are now removed when no
longer needed.

* Empty container databases (such as might be created on handoffs)
now shard much more quickly.

- |
The ``proxy-server`` now ignores 404 responses from handoffs that have
no data when deciding on the correct response for object requests,
similar to what it already does for account and container requests.

- |
Static Large Object sizes in listings for versioned containers are
now more accurate.

- |
When refetching Static Large Object manifests, non-manifest responses
are now handled better.

- |
S3 API now translates ``503 Service Unavailable`` responses to a more
S3-like response instead of raising an error.

- |
Improved proxy-to-backend requests to be more RFC-compliant.

- |
Various other minor bug fixes and improvements.

+ 2
- 0
releasenotes/source/index.rst View File

@@ -7,6 +7,8 @@

current

train

stein

rocky

+ 0
- 75
releasenotes/source/locale/ko_KR/LC_MESSAGES/releasenotes.po View File

@@ -1,75 +0,0 @@
# Sungjin Kang <gang.sungjin@gmail.com>, 2017. #zanata
msgid ""
msgstr ""
"Project-Id-Version: swift\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-25 00:41+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2017-02-07 03:09+0000\n"
"Last-Translator: Sungjin Kang <gang.sungjin@gmail.com>\n"
"Language-Team: Korean (South Korea)\n"
"Language: ko_KR\n"
"X-Generator: Zanata 4.3.3\n"
"Plural-Forms: nplurals=1; plural=0\n"

msgid "2.10.0"
msgstr "2.10.0"

msgid "2.10.1"
msgstr "2.10.1"

msgid "2.11.0"
msgstr "2.11.0"

msgid "2.12.0"
msgstr "2.12.0"

msgid "Bug Fixes"
msgstr "버그 수정"

msgid "Critical Issues"
msgstr "치명적인 이슈"

msgid "Current (Unreleased) Release Notes"
msgstr "현재 (릴리드전) 릴리즈 노트"

msgid "New Features"
msgstr "새로운 기능"

msgid "Newton Series Release Notes"
msgstr "Newton 시리즈 릴리즈 노트"

msgid "Other Notes"
msgstr "기타 기능"

msgid "Swift Release Notes"
msgstr "Swift 릴리즈 노트"

msgid ""
"Update dnspython dependency to 1.14, removing the need to have separate "
"dnspython dependencies for Py2 and Py3."
msgstr ""
"Dnspython 의존성을 1.14로 업그래이드 하여 Py2 와 Py3 에 대한 별도의 "
"dnspython 의존성을 제거할 필요가 없습니다."

msgid "Updated the PyECLib dependency to 1.3.1."
msgstr "PyECLib 의존성을 1.3.1 로 업그레이드 하였습니다."

msgid "Upgrade Notes"
msgstr "업그레이드 노트"

msgid "Various other minor bug fixes and improvements."
msgstr "다양한 다른 마이너 버그 수정 및 개선."

msgid ""
"WARNING: If you are using the ISA-L library for erasure codes, please "
"upgrade to liberasurecode 1.3.1 (or later) as soon as possible. If you are "
"using isa_l_rs_vand with more than 4 parity, please read https://bugs."
"launchpad.net/swift/+bug/1639691 and take necessary action."
msgstr ""
"경고: Erasure 코드에서 사용하는 ISA-L 라이브러리를 사용하는 경우, 최대한 빨"
"리 liberasurecode 1.3.1 (또는 그 이상) 으로 업그레이드하십시오. 4 parity 보"
"다 큰 isa_l_rs_vand 를 사용하는 경우, https://bugs.launchpad.net/swift/"
"+bug/1639691 을 읽고 필요한 조치를 취하십시오."

+ 6
- 0
releasenotes/source/train.rst View File

@@ -0,0 +1,6 @@
==========================
Train Series Release Notes
==========================

.. release-notes::
:branch: stable/train

+ 7
- 3
swift/account/backend.py View File

@@ -457,12 +457,16 @@ class AccountBroker(DatabaseBroker):
end = name.find(delimiter, len(prefix))
if end > 0:
if reverse:
end_marker = name[:end + 1]
end_marker = name[:end + len(delimiter)]
else:
marker = name[:end] + chr(ord(delimiter) + 1)
marker = ''.join([
name[:end],
delimiter[:-1],
chr(ord(delimiter[-1:]) + 1),
])
# we want result to be inclusive of delim+1
delim_force_gte = True
dir_name = name[:end + 1]
dir_name = name[:end + len(delimiter)]
if dir_name != orig_marker:
results.append([dir_name, 0, 0, '0', 1])
curs.close()

+ 0
- 3
swift/account/server.py View File

@@ -207,9 +207,6 @@ class AccountController(BaseStorageServer):
drive, part, account = split_and_validate_path(req, 3)
prefix = get_param(req, 'prefix')
delimiter = get_param(req, 'delimiter')
if delimiter and (len(delimiter) > 1 or ord(delimiter) > 254):
# delimiters can be made more flexible later
return HTTPPreconditionFailed(body='Bad delimiter')
limit = constraints.ACCOUNT_LISTING_LIMIT
given_limit = get_param(req, 'limit')
reverse = config_true_value(get_param(req, 'reverse'))

+ 1
- 1
swift/cli/ringbuilder.py View File

@@ -1167,7 +1167,7 @@ swift-ring-builder <ring_file> write_builder [min_part_hours]
'parts': ring.partition_count,
'devs': ring.devs,
'devs_changed': False,
'version': 0,
'version': ring.version or 0,
'_replica2part2dev': ring._replica2part2dev_id,
'_last_part_moves_epoch': None,
'_last_part_moves': None,

+ 10
- 1
swift/common/bufferedhttp.py View File

@@ -34,7 +34,7 @@ import socket
import eventlet
from eventlet.green.httplib import CONTINUE, HTTPConnection, HTTPMessage, \
HTTPResponse, HTTPSConnection, _UNKNOWN
from six.moves.urllib.parse import quote
from six.moves.urllib.parse import quote, parse_qsl, urlencode
import six

if six.PY2:
@@ -292,6 +292,15 @@ def http_connect_raw(ipaddr, port, method, path, headers=None,
else:
conn = BufferedHTTPConnection('%s:%s' % (ipaddr, port))
if query_string:
# Round trip to ensure proper quoting
if six.PY2:
query_string = urlencode(parse_qsl(
query_string, keep_blank_values=True))
else:
query_string = urlencode(
parse_qsl(query_string, keep_blank_values=True,
encoding='latin1'),
encoding='latin1')
path += '?' + query_string
conn.path = path
conn.putrequest(method, path, skip_host=(headers and 'Host' in headers))

+ 3
- 0
swift/common/manager.py View File

@@ -22,6 +22,7 @@ import signal
import time
import subprocess
import re
import six
from swift import gettext_ as _
import tempfile

@@ -721,6 +722,8 @@ class Server(object):
else:
output = proc.stdout.read()
proc.stdout.close()
if not six.PY2:
output = output.decode('utf8', 'backslashreplace')

if kwargs.get('once', False):
# if you don't want once to wait you can send it to the

+ 9
- 4
swift/common/middleware/s3api/controllers/multi_upload.py View File

@@ -543,10 +543,15 @@ class UploadController(Controller):

# Iterate over the segment objects and delete them individually
objects = json.loads(resp.body)
for o in objects:
container = req.container_name + MULTIUPLOAD_SUFFIX
obj = bytes_to_wsgi(o['name'].encode('utf-8'))
req.get_response(self.app, container=container, obj=obj)
while objects:
for o in objects:
container = req.container_name + MULTIUPLOAD_SUFFIX
obj = bytes_to_wsgi(o['name'].encode('utf-8'))
req.get_response(self.app, container=container, obj=obj)
query['marker'] = objects[-1]['name']
resp = req.get_response(self.app, 'GET', container, '',
query=query)
objects = json.loads(resp.body)

return HTTPNoContent()


+ 71
- 12
swift/common/middleware/s3api/s3api.py View File

@@ -19,12 +19,26 @@ To enable this middleware to your configuration, add the s3api middleware
in front of the auth middleware. See ``proxy-server.conf-sample`` for more
detail and configurable options.

To set up your client, the access key will be the concatenation of the
account and user strings that should look like test:tester, and the
secret access key is the account password. The host should also point
to the swift storage hostname.
To set up your client, ensure you are using the tempauth or keystone auth
system for swift project.
When your swift on a SAIO environment, make sure you have setting the tempauth
middleware configuration in ``proxy-server.conf``, and the access key will be
the concatenation of the account and user strings that should look like
test:tester, and the secret access key is the account password. The host should
also point to the swift storage hostname.

An example client using the python boto library is as follows::
The tempauth option example:

.. code-block:: ini

[filter:tempauth]
use = egg:swift#tempauth
user_admin_admin = admin .admin .reseller_admin
user_test_tester = testing

An example client using tempauth with the python boto library is as follows:

.. code-block:: python

from boto.s3.connection import S3Connection
connection = S3Connection(
@@ -35,6 +49,39 @@ An example client using the python boto library is as follows::
is_secure=False,
calling_format=boto.s3.connection.OrdinaryCallingFormat())

And if you using keystone auth, you need the ec2 credentials, which can
be downloaded from the API Endpoints tab of the dashboard or by openstack
ec2 command.

Here is showing to create an EC2 credential:

.. code-block:: console

# openstack ec2 credentials create
+------------+---------------------------------------------------+
| Field | Value |
+------------+---------------------------------------------------+
| access | c2e30f2cd5204b69a39b3f1130ca8f61 |
| links | {u'self': u'http://controller:5000/v3/......'} |
| project_id | 407731a6c2d0425c86d1e7f12a900488 |
| secret | baab242d192a4cd6b68696863e07ed59 |
| trust_id | None |
| user_id | 00f0ee06afe74f81b410f3fe03d34fbc |
+------------+---------------------------------------------------+

An example client using keystone auth with the python boto library will be:

.. code-block:: python

from boto.s3.connection import S3Connection
connection = S3Connection(
aws_access_key_id='c2e30f2cd5204b69a39b3f1130ca8f61',
aws_secret_access_key='baab242d192a4cd6b68696863e07ed59',
port=8080,
host='127.0.0.1',
is_secure=False,
calling_format=boto.s3.connection.OrdinaryCallingFormat())

----------
Deployment
----------
@@ -47,23 +94,35 @@ To enable all compatibility currently supported, you should make sure that
bulk, slo, and your auth middleware are also included in your proxy
pipeline setting.

Minimum example config is::
Using tempauth, the minimum example config is:

.. code-block:: ini

[pipeline:main]
pipeline = proxy-logging cache s3api tempauth bulk slo proxy-logging
proxy-server
pipeline = proxy-logging cache s3api tempauth bulk slo proxy-logging \
proxy-server

When using keystone, the config will be:

When using keystone, the config will be::
.. code-block:: ini

[pipeline:main]
pipeline = proxy-logging cache s3api s3token keystoneauth bulk slo
proxy-logging proxy-server
pipeline = proxy-logging cache authtoken s3api s3token keystoneauth bulk \
slo proxy-logging proxy-server

Finally, add the s3api middleware section:

.. code-block:: ini

[filter:s3api]
use = egg:swift#s3api

.. note::
``keystonemiddleware.authtoken`` can be located before/after s3api but
we recommend to put it before s3api because when authtoken is after s3api,
both authtoken and s3token will issue the acceptable token to keystone
(i.e. authenticate twice).
(i.e. authenticate twice). And in the ``keystonemiddleware.authtoken``
middleware , you should set ``delay_auth_decision`` option to ``True``.

-----------
Constraints

+ 8
- 10
swift/common/middleware/s3api/s3request.py View File

@@ -537,7 +537,6 @@ class S3Request(swob.Request):
'string_to_sign': self.string_to_sign,
'check_signature': self.check_signature,
}
self.token = None
self.account = None
self.user_id = None
self.slo_enabled = slo_enabled
@@ -1136,8 +1135,6 @@ class S3Request(swob.Request):
if method is not None:
env['REQUEST_METHOD'] = method

env['HTTP_X_AUTH_TOKEN'] = self.token

if obj:
path = '/v1/%s/%s/%s' % (account, container, obj)
elif container:
@@ -1329,7 +1326,7 @@ class S3Request(swob.Request):
except swob.HTTPException as err:
sw_resp = err
else:
# reuse account and tokens
# reuse account
_, self.account, _ = split_path(sw_resp.environ['PATH_INFO'],
2, 3, True)

@@ -1339,10 +1336,11 @@ class S3Request(swob.Request):
if not self.user_id:
if 'HTTP_X_USER_NAME' in sw_resp.environ:
# keystone
self.user_id = \
utf8encode("%s:%s" %
(sw_resp.environ['HTTP_X_TENANT_NAME'],
sw_resp.environ['HTTP_X_USER_NAME']))
self.user_id = "%s:%s" % (
sw_resp.environ['HTTP_X_TENANT_NAME'],
sw_resp.environ['HTTP_X_USER_NAME'])
if six.PY2 and not isinstance(self.user_id, bytes):
self.user_id = self.user_id.encode('utf8')
else:
# tempauth
self.user_id = self.access_key
@@ -1505,8 +1503,8 @@ class S3AclRequest(S3Request):
# keystone
self.user_id = "%s:%s" % (sw_resp.environ['HTTP_X_TENANT_NAME'],
sw_resp.environ['HTTP_X_USER_NAME'])
self.user_id = utf8encode(self.user_id)
self.token = sw_resp.environ.get('HTTP_X_AUTH_TOKEN')
if six.PY2 and not isinstance(self.user_id, bytes):
self.user_id = self.user_id.encode('utf8')
else:
# tempauth
self.user_id = self.access_key

+ 12
- 10
swift/common/middleware/s3api/s3token.py View File

@@ -111,10 +111,7 @@ def parse_v2_response(token):
'X-Project-Id': access_info['token']['tenant']['id'],
'X-Project-Name': access_info['token']['tenant']['name'],
}
return (
headers,
access_info['token'].get('id'),
access_info['token']['tenant'])
return headers, access_info['token']['tenant']


def parse_v3_response(token):
@@ -134,7 +131,7 @@ def parse_v3_response(token):
'X-Project-Domain-Id': token['project']['domain']['id'],
'X-Project-Domain-Name': token['project']['domain']['name'],
}
return headers, None, token['project']
return headers, token['project']


class S3Token(object):
@@ -308,7 +305,13 @@ class S3Token(object):
if memcache_client:
cached_auth_data = memcache_client.get(memcache_token_key)
if cached_auth_data:
headers, token_id, tenant, secret = cached_auth_data
if len(cached_auth_data) == 4:
# Old versions of swift may have cached token, too,
# but we don't need it
headers, _token, tenant, secret = cached_auth_data
else:
headers, tenant, secret = cached_auth_data

if s3_auth_details['check_signature'](secret):
self._logger.debug("Cached creds valid")
else:
@@ -348,9 +351,9 @@ class S3Token(object):
try:
token = resp.json()
if 'access' in token:
headers, token_id, tenant = parse_v2_response(token)
headers, tenant = parse_v2_response(token)
elif 'token' in token:
headers, token_id, tenant = parse_v3_response(token)
headers, tenant = parse_v3_response(token)
else:
raise ValueError
if memcache_client:
@@ -363,7 +366,7 @@ class S3Token(object):
access=access)
memcache_client.set(
memcache_token_key,
(headers, token_id, tenant, cred_ref.secret),
(headers, tenant, cred_ref.secret),
time=self._secret_cache_duration)
self._logger.debug("Cached keystone credentials")
except Exception:
@@ -391,7 +394,6 @@ class S3Token(object):
environ, start_response)

req.headers.update(headers)
req.headers['X-Auth-Token'] = token_id
tenant_to_connect = force_tenant or tenant['id']
if six.PY2 and isinstance(tenant_to_connect, six.text_type):
tenant_to_connect = tenant_to_connect.encode('utf-8')

+ 2
- 0
swift/common/middleware/s3api/subresource.py View File

@@ -232,6 +232,8 @@ class Owner(object):
"""
def __init__(self, id, name):
self.id = id
if not (name is None or isinstance(name, six.string_types)):
raise TypeError('name must be a string or None')
self.name = name



+ 6
- 1
swift/common/middleware/versioned_writes.py View File

@@ -383,13 +383,18 @@ class VersionedWritesContext(WSGIContext):
return source_resp

def _put_versioned_obj(self, req, put_path_info, source_resp):
# Create a new Request object to PUT to the versions container, copying
# Create a new Request object to PUT to the container, copying
# all headers from the source object apart from x-timestamp.
put_req = make_pre_authed_request(
req.environ, path=wsgi_quote(put_path_info), method='PUT',
swift_source='VW')
copy_header_subset(source_resp, put_req,
lambda k: k.lower() != 'x-timestamp')
slo_size = put_req.headers.get('X-Object-Sysmeta-Slo-Size')
if slo_size:
put_req.headers['Content-Type'] += '; swift_bytes=' + slo_size
put_req.environ['swift.content_type_overridden'] = True

put_req.environ['wsgi.input'] = FileLikeIter(source_resp.app_iter)
put_resp = put_req.get_response(self.app)
close_if_possible(source_resp.app_iter)

+ 4
- 2
swift/common/ring/builder.py View File

@@ -364,13 +364,15 @@ class RingBuilder(object):
# shift an unsigned int >I right to obtain the partition for the
# int).
if not self._replica2part2dev:
self._ring = RingData([], devs, self.part_shift)
self._ring = RingData([], devs, self.part_shift,
version=self.version)
else:
self._ring = \
RingData([array('H', p2d) for p2d in
self._replica2part2dev],
devs, self.part_shift,
self.next_part_power)
self.next_part_power,
self.version)
return self._ring

def add_dev(self, dev):

+ 120
- 9
swift/common/ring/ring.py View File

@@ -22,11 +22,11 @@ from os.path import getmtime
import struct
from time import time
import os
from io import BufferedReader
from hashlib import md5
from itertools import chain, count
from tempfile import NamedTemporaryFile
import sys
import zlib

from six.moves import range

@@ -41,15 +41,77 @@ def calc_replica_count(replica2part2dev_id):
return base + extra


class RingReader(object):
chunk_size = 2 ** 16

def __init__(self, filename):
self.fp = open(filename, 'rb')
self._reset()

def _reset(self):
self._buffer = b''
self.size = 0
self.raw_size = 0
self._md5 = md5()
self._decomp = zlib.decompressobj(32 + zlib.MAX_WBITS)

@property
def close(self):
return self.fp.close

def seek(self, pos, ref=0):
if (pos, ref) != (0, 0):
raise NotImplementedError
self._reset()
return self.fp.seek(pos, ref)

def _buffer_chunk(self):
chunk = self.fp.read(self.chunk_size)
if not chunk:
return False
self.size += len(chunk)
self._md5.update(chunk)
chunk = self._decomp.decompress(chunk)
self.raw_size += len(chunk)
self._buffer += chunk
return True

def read(self, amount=-1):
if amount < 0:
raise IOError("don't be greedy")

while amount > len(self._buffer):
if not self._buffer_chunk():
break

result, self._buffer = self._buffer[:amount], self._buffer[amount:]
return result

def readline(self):
# apparently pickle needs this?
while b'\n' not in self._buffer:
if not self._buffer_chunk():
break

line, sep, self._buffer = self._buffer.partition(b'\n')
return line + sep

@property
def md5(self):
return self._md5.hexdigest()


class RingData(object):
"""Partitioned consistent hashing ring data (used for serialization)."""

def __init__(self, replica2part2dev_id, devs, part_shift,
next_part_power=None):
next_part_power=None, version=None):
self.devs = devs
self._replica2part2dev_id = replica2part2dev_id
self._part_shift = part_shift
self.next_part_power = next_part_power
self.version = version
self.md5 = self.size = self.raw_size = None

for dev in self.devs:
if dev is not None:
@@ -104,7 +166,7 @@ class RingData(object):
:param bool metadata_only: If True, only load `devs` and `part_shift`.
:returns: A RingData instance containing the loaded data.
"""
gz_file = BufferedReader(GzipFile(filename, 'rb'))
gz_file = RingReader(filename)

# See if the file is in the new format
magic = gz_file.read(4)
@@ -124,7 +186,10 @@ class RingData(object):
if not hasattr(ring_data, 'devs'):
ring_data = RingData(ring_data['replica2part2dev_id'],
ring_data['devs'], ring_data['part_shift'],
ring_data.get('next_part_power'))
ring_data.get('next_part_power'),
ring_data.get('version'))
for attr in ('md5', 'size', 'raw_size'):
setattr(ring_data, attr, getattr(gz_file, attr))
return ring_data

def serialize_v1(self, file_obj):
@@ -138,6 +203,9 @@ class RingData(object):
'replica_count': len(ring['replica2part2dev_id']),
'byteorder': sys.byteorder}

if ring['version'] is not None:
_text['version'] = ring['version']

next_part_power = ring.get('next_part_power')
if next_part_power is not None:
_text['next_part_power'] = next_part_power
@@ -175,7 +243,8 @@ class RingData(object):
return {'devs': self.devs,
'replica2part2dev_id': self._replica2part2dev_id,
'part_shift': self._part_shift,
'next_part_power': self.next_part_power}
'next_part_power': self.next_part_power,
'version': self.version}


class Ring(object):
@@ -239,6 +308,10 @@ class Ring(object):
self._rebuild_tier_data()
self._update_bookkeeping()
self._next_part_power = ring_data.next_part_power
self._version = ring_data.version
self._md5 = ring_data.md5
self._size = ring_data.size
self._raw_size = ring_data.raw_size

def _update_bookkeeping(self):
# Do this now, when we know the data has changed, rather than
@@ -257,12 +330,19 @@ class Ring(object):
zones = set()
ips = set()
self._num_devs = 0
self._num_assigned_devs = 0
self._num_weighted_devs = 0
for dev in self._devs:
if dev and dev['id'] in dev_ids_with_parts:
if dev is None:
continue
self._num_devs += 1
if dev.get('weight', 0) > 0:
self._num_weighted_devs += 1
if dev['id'] in dev_ids_with_parts:
regions.add(dev['region'])
zones.add((dev['region'], dev['zone']))
ips.add((dev['region'], dev['zone'], dev['ip']))
self._num_devs += 1
self._num_assigned_devs += 1
self._num_regions = len(regions)
self._num_zones = len(zones)
self._num_ips = len(ips)
@@ -275,6 +355,22 @@ class Ring(object):
def part_power(self):
return 32 - self._part_shift

@property
def version(self):
return self._version

@property
def md5(self):
return self._md5

@property
def size(self):
return self._size

@property
def raw_size(self):
return self._raw_size

def _rebuild_tier_data(self):
self.tier2devs = defaultdict(list)
for dev in self._devs:
@@ -301,6 +397,21 @@ class Ring(object):
"""Number of partitions in the ring."""
return len(self._replica2part2dev_id[0])

@property
def device_count(self):
"""Number of devices in the ring."""
return self._num_devs

@property
def weighted_device_count(self):
"""Number of devices with weight in the ring."""
return self._num_weighted_devs

@property
def assigned_device_count(self):
"""Number of devices with assignments in the ring."""
return self._num_assigned_devs

@property
def devs(self):
"""devices in the ring"""
@@ -490,7 +601,7 @@ class Ring(object):
hit_all_ips = True
break

hit_all_devs = len(used) == self._num_devs
hit_all_devs = len(used) == self._num_assigned_devs
for handoff_part in chain(range(start, parts, inc),
range(inc - ((parts - start) % inc),
start, inc)):
@@ -505,6 +616,6 @@ class Ring(object):
dev = self._devs[dev_id]
yield dict(dev, handoff_index=next(index))
used.add(dev_id)
if len(used) == self._num_devs:
if len(used) == self._num_assigned_devs:
hit_all_devs = True
break

+ 37
- 0
swift/common/wsgi.py View File

@@ -32,6 +32,7 @@ from eventlet.green import socket, ssl, os as green_os
import six
from six import BytesIO
from six import StringIO
from six.moves import configparser

from swift.common import utils, constraints
from swift.common.storage_policy import BindPortsCache
@@ -55,6 +56,23 @@ except (ImportError, NotImplementedError):
CPU_COUNT = 1


if not six.PY2:
# In general, we haven't really thought much about interpolation in
# configs. Python's default ConfigParser has always supported it, though,
# so *we* got it "for free". Unfortunatley, since we "supported"
# interpolation, we have to assume there are deployments in the wild that
# use it, and try not to break them. So, do what we can to mimic the py2
# behavior of passing through values like "1%" (which we want to support
# for fallocate_reserve).
class NicerInterpolation(configparser.BasicInterpolation):
def before_get(self, parser, section, option, value, defaults):
if '%(' not in value:
return value
return super(NicerInterpolation, self).before_get(
parser, section, option, value, defaults)
configparser.ConfigParser._DEFAULT_INTERPOLATION = NicerInterpolation()


class NamedConfigLoader(loadwsgi.ConfigLoader):
"""
Patch paste.deploy's ConfigLoader so each context object will know what
@@ -480,6 +498,11 @@ class SwiftHttpProtocol(wsgi.HttpProtocol):
break
header, value = line.split(':', 1)
value = value.strip(' \t\n\r')
# NB: Eventlet looks at the headers obj to figure out
# whether the client said the connection should close;
# see https://github.com/eventlet/eventlet/blob/v0.25.0/
# eventlet/wsgi.py#L504
self.headers.add_header(header, value)
headers_raw.append((header, value))
wsgi_key = 'HTTP_' + header.replace('-', '_').encode(
'latin1').upper().decode('latin1')
@@ -488,6 +511,20 @@ class SwiftHttpProtocol(wsgi.HttpProtocol):
wsgi_key = wsgi_key[5:]
environ[wsgi_key] = value
environ['headers_raw'] = tuple(headers_raw)
# Since we parsed some more headers, check to see if they
# change how our wsgi.input should behave
te = environ.get('HTTP_TRANSFER_ENCODING', '').lower()
if te.rsplit(',', 1)[-1].strip() == 'chunked':
environ['wsgi.input'].chunked_input = True
else:
length = environ.get('CONTENT_LENGTH')
if length:
length = int(length)
environ['wsgi.input'].content_length = length
if environ.get('HTTP_EXPECT', '').lower() == '100-continue':
environ['wsgi.input'].wfile = self.wfile
environ['wsgi.input'].wfile_line = \
b'HTTP/1.1 100 Continue\r\n'
return environ



+ 33
- 11
swift/container/backend.py View File

@@ -1186,19 +1186,27 @@ class ContainerBroker(DatabaseBroker):
continue
if end >= 0 and len(name) > end + len(delimiter):
if reverse:
end_marker = name[:end + 1]
end_marker = name[:end + len(delimiter)]
else:
marker = name[:end] + chr(ord(delimiter) + 1)
marker = ''.join([
name[:end],
delimiter[:-1],
chr(ord(delimiter[-1:]) + 1),
])
curs.close()
break
elif end >= 0:
if reverse:
end_marker = name[:end + 1]
end_marker = name[:end + len(delimiter)]
else:
marker = name[:end] + chr(ord(delimiter) + 1)
marker = ''.join([
name[:end],
delimiter[:-1],
chr(ord(delimiter[-1:]) + 1),
])
# we want result to be inclusive of delim+1
delim_force_gte = True
dir_name = name[:end + 1]
dir_name = name[:end + len(delimiter)]
if dir_name != orig_marker:
results.append([dir_name, '0', 0, None, ''])
curs.close()
@@ -1996,6 +2004,22 @@ class ContainerBroker(DatabaseBroker):
self.update_metadata({'X-Container-Sysmeta-Shard-' + key:
(value, Timestamp.now().internal)})

def get_sharding_sysmeta_with_timestamps(self):
"""
Returns sharding specific info from the broker's metadata with
timestamps.

:param key: if given the value stored under ``key`` in the sharding
info will be returned.
:return: a dict of sharding info with their timestamps.
"""
prefix = 'X-Container-Sysmeta-Shard-'
return {
k[len(prefix):]: v
for k, v in self.metadata.items()
if k.startswith(prefix)
}

def get_sharding_sysmeta(self, key=None):
"""
Returns sharding specific info from the broker's metadata.
@@ -2005,13 +2029,11 @@ class ContainerBroker(DatabaseBroker):
:return: either a dict of sharding info or the value stored under
``key`` in that dict.
"""
prefix = 'X-Container-Sysmeta-Shard-'
metadata = self.metadata
info = dict((k[len(prefix):], v[0]) for
k, v in metadata.items() if k.startswith(prefix))
info = self.get_sharding_sysmeta_with_timestamps()
if key:
return info.get(key)
return info
return info.get(key, (None, None))[0]
else:
return {k: v[0] for k, v in info.items()}

def _load_root_info(self):
"""

+ 0
- 3
swift/container/server.py View File

@@ -637,9 +637,6 @@ class ContainerController(BaseStorageServer):
path = get_param(req, 'path')
prefix = get_param(req, 'prefix')
delimiter = get_param(req, 'delimiter')
if delimiter and (len(delimiter) > 1 or ord(delimiter) > 254):
# delimiters can be made more flexible later
return HTTPPreconditionFailed(body='Bad delimiter')
marker = get_param(req, 'marker', '')
end_marker = get_param(req, 'end_marker')
limit = constraints.CONTAINER_LISTING_LIMIT

+ 91
- 16
swift/container/sharder.py View File

@@ -40,6 +40,11 @@ from swift.container.backend import ContainerBroker, \
from swift.container.replicator import ContainerReplicator


CLEAVE_SUCCESS = 0
CLEAVE_FAILED = 1
CLEAVE_EMPTY = 2


def sharding_enabled(broker):
# NB all shards will by default have been created with
# X-Container-Sysmeta-Sharding set and will therefore be candidates for
@@ -220,6 +225,10 @@ class CleavingContext(object):
yield 'ranges_done', self.ranges_done
yield 'ranges_todo', self.ranges_todo

def __repr__(self):
return '%s(%s)' % (self.__class__.__name__, ', '.join(
'%s=%r' % prop for prop in self))

def _encode(cls, value):
if value is not None and six.PY2 and isinstance(value, six.text_type):
return value.encode('utf-8')
@@ -241,6 +250,26 @@ class CleavingContext(object):
def _make_ref(cls, broker):
return broker.get_info()['id']

@classmethod
def load_all(cls, broker):
"""
Returns all cleaving contexts stored in the broker.

:param broker:
:return: list of tuples of (CleavingContext, timestamp)
"""
brokers = broker.get_brokers()
sysmeta = brokers[-1].get_sharding_sysmeta_with_timestamps()

for key, (val, timestamp) in sysmeta.items():
# If the value is of length 0, then the metadata is
# marked for deletion
if key.startswith("Context-") and len(val) > 0:
try:
yield cls(**json.loads(val)), timestamp
except ValueError:
continue

@classmethod
def load(cls, broker):
"""
@@ -287,6 +316,11 @@ class CleavingContext(object):
return all((self.misplaced_done, self.cleaving_done,
self.max_row == self.cleave_to_row))

def delete(self, broker):
# These will get reclaimed when `_reclaim_metadata` in
# common/db.py is called.
broker.set_sharding_sysmeta('Context-' + self.ref, '')


DEFAULT_SHARD_CONTAINER_THRESHOLD = 1000000
DEFAULT_SHARD_SHRINK_POINT = 25
@@ -607,6 +641,7 @@ class ContainerSharder(ContainerReplicator):
"""
part = self.ring.get_part(shard_range.account, shard_range.container)
node = self.find_local_handoff_for_part(part)
put_timestamp = Timestamp.now().internal
if not node:
raise DeviceUnavailable(
'No mounted devices found suitable for creating shard broker '
@@ -615,7 +650,7 @@ class ContainerSharder(ContainerReplicator):
shard_broker = ContainerBroker.create_broker(
os.path.join(self.root, node['device']), part, shard_range.account,
shard_range.container, epoch=shard_range.epoch,
storage_policy_index=policy_index)
storage_policy_index=policy_index, put_timestamp=put_timestamp)

# Get the valid info into the broker.container, etc
shard_broker.get_info()
@@ -625,7 +660,7 @@ class ContainerSharder(ContainerReplicator):
'X-Container-Sysmeta-Sharding':
('True', Timestamp.now().internal)})

return part, shard_broker, node['id']
return part, shard_broker, node['id'], put_timestamp

def _audit_root_container(self, broker):
# This is the root container, and therefore the tome of knowledge,
@@ -724,12 +759,20 @@ class ContainerSharder(ContainerReplicator):
self._increment_stat('audit_shard', 'success', statsd=True)
return True

def _audit_cleave_contexts(self, broker):
now = Timestamp.now()
for context, last_mod in CleavingContext.load_all(broker):
if Timestamp(last_mod).timestamp + self.reclaim_age < \
now.timestamp:
context.delete(broker)

def _audit_container(self, broker):
if broker.is_deleted():
# if the container has been marked as deleted, all metadata will
# have been erased so no point auditing. But we want it to pass, in
# case any objects exist inside it.
return True
self._audit_cleave_contexts(broker)
if broker.is_root_container():
return self._audit_root_container(broker)
return self._audit_shard_container(broker)
@@ -804,7 +847,7 @@ class ContainerSharder(ContainerReplicator):
last_index = next_index = 0
for obj in objs:
if dest_shard_range is None:
# no more destinations: yield remainder of batch and return
# no more destinations: yield remainder of batch and bail
# NB there may be more batches of objects but none of them
# will be placed so no point fetching them
yield objs[last_index:], None, info
@@ -893,7 +936,7 @@ class ContainerSharder(ContainerReplicator):
continue

if dest_shard_range not in dest_brokers:
part, dest_broker, node_id = self._get_shard_broker(
part, dest_broker, node_id, _junk = self._get_shard_broker(
dest_shard_range, src_broker.root_path, policy_index)
# save the broker info that was sampled prior to the *first*
# yielded objects for this destination
@@ -1122,12 +1165,15 @@ class ContainerSharder(ContainerReplicator):
start = time.time()
policy_index = broker.storage_policy_index
try:
shard_part, shard_broker, node_id = self._get_shard_broker(
shard_range, broker.root_path, policy_index)
shard_part, shard_broker, node_id, put_timestamp = \
self._get_shard_broker(shard_range, broker.root_path,
policy_index)
except DeviceUnavailable as duex:
self.logger.warning(str(duex))
self._increment_stat('cleaved', 'failure', statsd=True)
return False
return CLEAVE_FAILED

own_shard_range = broker.get_own_shard_range()

# only cleave from the retiring db - misplaced objects handler will
# deal with any objects in the fresh db
@@ -1138,13 +1184,36 @@ class ContainerSharder(ContainerReplicator):
source_db_id = source_broker.get_info()['id']
source_max_row = source_broker.get_max_row()
sync_point = shard_broker.get_sync(source_db_id)
if sync_point < source_max_row:
if sync_point < source_max_row or source_max_row == -1:
sync_from_row = max(cleaving_context.last_cleave_to_row or -1,
sync_point)
objects = None
for objects, info in self.yield_objects(
source_broker, shard_range,
since_row=sync_from_row):
shard_broker.merge_items(objects)
if objects is None:
self.logger.info("Cleaving '%s': %r - zero objects found",
broker.path, shard_range)
if shard_broker.get_info()['put_timestamp'] == put_timestamp:
# This was just created; don't need to replicate this
# SR because there was nothing there. So cleanup and
# remove the shard_broker from its hand off location.
self.delete_db(shard_broker)
cleaving_context.cursor = shard_range.upper_str
cleaving_context.ranges_done += 1
cleaving_context.ranges_todo -= 1
if shard_range.upper >= own_shard_range.upper:
# cleaving complete
cleaving_context.cleaving_done = True
cleaving_context.store(broker)
# Because nothing was here we wont count it in the shard
# batch count.
return CLEAVE_EMPTY
# Else, it wasn't newly created by us, and
# we don't know what's in it or why. Let it get
# replicated and counted in the batch count.

# Note: the max row stored as a sync point is sampled *before*
# objects are yielded to ensure that is less than or equal to
# the last yielded row. Other sync points are also copied from the
@@ -1159,8 +1228,6 @@ class ContainerSharder(ContainerReplicator):
self.logger.debug("Cleaving '%s': %r - shard db already in sync",
broker.path, shard_range)

own_shard_range = broker.get_own_shard_range()

replication_quorum = self.existing_shard_replication_quorum
if shard_range.includes(own_shard_range):
# When shrinking, include deleted own (donor) shard range in
@@ -1202,7 +1269,7 @@ class ContainerSharder(ContainerReplicator):
'%s successes, %s required.', shard_range, broker.path,
replication_successes, replication_quorum)
self._increment_stat('cleaved', 'failure', statsd=True)
return False
return CLEAVE_FAILED

elapsed = round(time.time() - start, 3)
self._min_stat('cleaved', 'min_time', elapsed)
@@ -1219,7 +1286,7 @@ class ContainerSharder(ContainerReplicator):
'Cleaved %s for shard range %s in %gs.',
broker.path, shard_range, elapsed)
self._increment_stat('cleaved', 'success', statsd=True)
return True
return CLEAVE_SUCCESS

def _cleave(self, broker):
# Returns True if misplaced objects have been moved and the entire
@@ -1264,23 +1331,30 @@ class ContainerSharder(ContainerReplicator):
cleaving_context.ranges_todo, broker.path)

ranges_done = []
for shard_range in ranges_todo[:self.cleave_batch_size]:
for shard_range in ranges_todo:
if shard_range.state == ShardRange.FOUND:
break
elif shard_range.state in (ShardRange.CREATED,
ShardRange.CLEAVED,
ShardRange.ACTIVE):
if self._cleave_shard_range(
broker, cleaving_context, shard_range):
cleave_result = self._cleave_shard_range(
broker, cleaving_context, shard_range)
if cleave_result == CLEAVE_SUCCESS:
ranges_done.append(shard_range)
else:
if len(ranges_done) == self.cleave_batch_size:
break
elif cleave_result == CLEAVE_FAILED:
break
# else, no errors, but no rows found either. keep going,
# and don't count it against our batch size
else:
self.logger.warning('Unexpected shard range state for cleave',
shard_range.state)
break

if not ranges_done:
# _cleave_shard_range always store()s the context on success; make
# sure we *also* do that if we hit a failure right off the bat
cleaving_context.store(broker)
self.logger.debug(
'Cleaved %s shard ranges for %s', len(ranges_done), broker.path)
@@ -1307,6 +1381,7 @@ class ContainerSharder(ContainerReplicator):
modified_shard_ranges.append(own_shard_range)
broker.merge_shard_ranges(modified_shard_ranges)
if broker.set_sharded_state():
cleaving_context.delete(broker)
return True
else:
self.logger.warning(

+ 210
- 68
swift/locale/de/LC_MESSAGES/swift.po View File

@@ -7,15 +7,16 @@
# Ettore Atalan <atalanttore@googlemail.com>, 2014-2015
# Jonas John <jonas.john@e-werkzeug.eu>, 2015
# Andreas Jaeger <jaegerandi@gmail.com>, 2016. #zanata
# Andreas Jaeger <jaegerandi@gmail.com>, 2019. #zanata
msgid ""
msgstr ""
"Project-Id-Version: swift VERSION\n"
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
"POT-Creation-Date: 2018-08-09 00:12+0000\n"
"POT-Creation-Date: 2019-10-04 06:59+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2016-06-02 07:02+0000\n"
"PO-Revision-Date: 2019-10-03 06:49+0000\n"
"Last-Translator: Andreas Jaeger <jaegerandi@gmail.com>\n"
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -57,10 +58,26 @@ msgstr ""
"%(replicated)d/%(total)d (%(percentage).2f%%) Partitionen repliziert in "
"%(time).2fs (%(rate).2f/s, %(remaining)s verbleibend)"

#, python-format
msgid "%(replication_ip)s/%(device)s responded as unmounted"
msgstr "%(replication_ip)s/%(device)s antwortet als unmounted"

#, python-format
msgid "%(server)s #%(number)d not running (%(conf)s)"
msgstr "%(server)s #%(number)d wird nicht ausgeführt (%(conf)s)"

#, python-format
msgid "%(server)s (%(pid)s) appears to have stopped"
msgstr "%(server)s (%(pid)s) scheinbar gestoppt"

#, python-format
msgid "%(server)s running (%(pid)s - %(conf)s)"
msgstr "%(server)s wird ausgeführt (%(pid)s - %(conf)s)"

#, python-format
msgid "%(server)s running (%(pid)s - %(pid_file)s)"
msgstr "%(server)s wird ausgeführt (%(pid)s - %(pid_file)s)"

#, python-format
msgid "%(success)s successes, %(failure)s failures"
msgstr "%(success)s Erfolge, %(failure)s Fehlschläge"
@@ -93,44 +110,9 @@ msgstr "%s zurückgemeldet als ausgehängt"
msgid "%s: Connection reset by peer"
msgstr "%s: Verbindung zurückgesetzt durch Peer"

#, python-format
msgid ", %s containers deleted"
msgstr ", %s Container gelöscht"

#, python-format
msgid ", %s containers possibly remaining"
msgstr ", %s Container möglicherweise verbleibend"

#, python-format
msgid ", %s containers remaining"
msgstr ", %s Container verbleibend"

#, python-format
msgid ", %s objects deleted"
msgstr ", %s Objekte gelöscht"

#, python-format
msgid ", %s objects possibly remaining"
msgstr ", %s Objekte möglicherweise verbleibend"

#, python-format
msgid ", %s objects remaining"
msgstr ", %s Objekte verbleibend"

#, python-format
msgid ", elapsed: %.02fs"
msgstr ", vergangen: %.02fs"

msgid ", return codes: "
msgstr ", Rückgabecodes: "

msgid "Account"
msgstr "Konto"

#, python-format
msgid "Account %(account)s has not been reaped since %(time)s"
msgstr "Konto %(account)s wurde nicht aufgeräumt seit %(time)s"

#, python-format
msgid "Account audit \"once\" mode completed: %.02fs"
msgstr "Kontoprüfungsmodus \"once\" abgeschlossen: %.02fs"
@@ -139,6 +121,13 @@ msgstr "Kontoprüfungsmodus \"once\" abgeschlossen: %.02fs"
msgid "Account audit pass completed: %.02fs"
msgstr "Kontoprüfungsdurchlauf abgeschlossen: %.02fs"

#, python-format
msgid ""
"Adding required filter %(filter_name)s to pipeline at position %(insert_at)d"
msgstr ""
"Füge erforderlichen Filter %(filter_name)s zu Pipeline an Position "
"%(insert_at)d hinzu"

#, python-format
msgid ""
"Attempted to replicate %(count)d dbs in %(time).5f seconds (%(rate).5f/s)"
@@ -150,6 +139,14 @@ msgstr ""
msgid "Audit Failed for %(path)s: %(err)s"
msgstr "Prüfung fehlgeschlagen für %(path)s: %(err)s"

#, python-format
msgid "Audit passed for %s"
msgstr "Prüfung für %s erfolgt"

#, python-format
msgid "Bad key for %(name)r: %(err)s"
msgstr "Schlechter Schlüssel für %(name)r: %(err)s"

#, python-format
msgid "Bad rsync return code: %(ret)d <- %(args)s"
msgstr "Falscher rsync-Rückgabecode: %(ret)d <- %(args)s"
@@ -181,10 +178,6 @@ msgstr "Einzelthread-Scanvorgang für Objektaktualisierung wird gestartet"
msgid "Begin object update sweep"
msgstr "Scanvorgang für Objektaktualisierung wird gestartet"

#, python-format
msgid "Beginning pass on account %s"
msgstr "Durchlauf für Konto %s wird gestartet"

msgid "Beginning replication run"
msgstr "Replizierungsdurchlauf wird gestartet"

@@ -242,6 +235,9 @@ msgstr ""
msgid "Connection refused"
msgstr "Verbindung abgelehnt"

msgid "Connection reset"
msgstr "Verbindung zurückgesetzt"

msgid "Connection timeout"
msgstr "Verbindungszeitüberschreitung"

@@ -260,6 +256,18 @@ msgstr "Containerprüfungsdurchlauf abgeschlossen: %.02fs"
msgid "Container sync \"once\" mode completed: %.02fs"
msgstr "Containersynchronisationsmodus \"once\" abgeschlossen: %.02fs"

#, python-format
msgid ""
"Container sync report: %(container)s, time window start: %(start)s, time "
"window end: %(end)s, puts: %(puts)s, posts: %(posts)s, deletes: %(deletes)s, "
"bytes: %(bytes)s, sync_point1: %(point1)s, sync_point2: %(point2)s, "
"total_rows: %(total)s"
msgstr ""
"Container Synchronisierungsbericht: %(container)s, Beginn Zeitfenster: "
"%(start)s, Ende Zeitfenster: %(end)s, puts: %(puts)s, posts: %(posts)s, "
"deletes: %(deletes)s, bytes: %(bytes)s, sync_point1: %(point1)s, "
"sync_point2: %(point2)s, total_rows: %(total)s"

#, python-format
msgid ""
"Container update single threaded sweep completed: %(elapsed).02fs, "
@@ -282,6 +290,10 @@ msgstr ""
"%(elapsed).02fs, %(success)s Erfolge, %(fail)s Fehler, %(no_change)s ohne "
"Änderungen"

#, python-format
msgid "Could not autocreate account %r"
msgstr "Kann das Konto %r nicht automatisch erstellen"

#, python-format
msgid ""
"Could not bind to %(addr)s:%(port)s after trying for %(timeout)s seconds"
@@ -297,10 +309,6 @@ msgstr "%(conf)r konnte nicht geladen werden: %(error)s"