diff --git a/doc/source/apache_deployment_guide.rst b/doc/source/apache_deployment_guide.rst new file mode 100644 index 0000000000..bdf0546876 --- /dev/null +++ b/doc/source/apache_deployment_guide.rst @@ -0,0 +1,178 @@ +======================= +Apache Deployment Guide +======================= + +---------------------------- +Web Front End Considerations +---------------------------- + +Swift can be configured to work both using an integral web front-end +and using a full-fledged Web Server such as the Apache2 (HTTPD) web server. +The integral web front-end is a wsgi mini "Web Server" which opens +up its own socket and serves http requests directly. +The incoming requests accepted by the integral web front-end are then forwarded +to a wsgi application (the core swift) for further handling, possibly +via wsgi middleware sub-components. + +client<---->'integral web front-end'<---->middleware<---->'core swift' + +To gain full advantage of Apache2, Swift can alternatively be +configured to work as a request processor of the Apache2 server. +This alternative deployment scenario uses mod_wsgi of Apache2 +to forward requests to the swift wsgi application and middleware. + +client<---->'Apache2 with mod_wsgi'<----->middleware<---->'core swift' + +The integral web front-end offers simplicity and requires +minimal configuration. It is also the web front-end most commonly used +with Swift. +Additionlly, the integral web front-end includes support for +receiving chunked transfer encoding from a client, +presently not supported by Apache2 in the operation mode described here. + +The use of Apache2 offers new ways to extend Swift and integrate it with +existing authentication, administration and control systems. +A single Apache2 server can serve as the web front end of any number of swift +servers residing on a swift node. +For example when a storage node offers account, container and object services, +a single Apache2 server can serve as the web front end of all three services. + +The apache variant described here was tested as part of an IBM research work. +It was found that following tuning, the Apache2 offer generally equivalent +performance to that offered by the integral web front-end. +Alternative to Apache2, other web servers may be used, but were never tested. + +------------- +Apache2 Setup +------------- +Both Apache2 and mod-wsgi needs to be installed on the system. +Ubuntu comes with Apache2 installed. Install mod-wsgi using:: + + sudo apt-get install libapache2-mod-wsgi + +First, change the User and Group IDs of Apache2 to be those used by Swift. +For example in /etc/apache2/envvars use:: + + export APACHE_RUN_USER=swift + export APACHE_RUN_GROUP=swift + +Create a directory for the Apache2 wsgi files:: + + sudo mkdir /var/www/swift + +Create a file for each service under /var/www/swift. + +For a proxy service create /var/www/swift/proxy-server.wsgi:: + + from swift.common.wsgi import init_request_processor + application, conf, logger, log_name = \ + init_request_processor('/etc/swift/proxy-server.conf','proxy-server') + +For an account service create /var/www/swift/account-server.wsgi:: + + from swift.common.wsgi import init_request_processor + application, conf, logger, log_name = \ + init_request_processor('/etc/swift/account-server.conf', + 'account-server') + +For an container service create /var/www/swift/container-server.wsgi:: + + from swift.common.wsgi import init_request_processor + application, conf, logger, log_name = \ + init_request_processor('/etc/swift/container-server.conf', + 'container-server') + +For an object service create /var/www/swift/object-server.wsgi:: + + from swift.common.wsgi import init_request_processor + application, conf, logger, log_name = \ + init_request_processor('/etc/swift/object-server.conf', + 'object-server') + +Create a /etc/apache2/conf.d/swift_wsgi.conf configuration file that will +define a port and Virtual Host per each local service. +For example an Apache2 serving as a web front end of a proxy service:: + + #Proxy + NameVirtualHost *:8080 + Listen 8080 + + ServerName proxy-server + LimitRequestBody 5368709122 + WSGIDaemonProcess proxy-server processes=5 threads=1 + WSGIProcessGroup proxy-server + WSGIScriptAlias / /var/www/swift/proxy-server.wsgi + LimitRequestFields 200 + ErrorLog /var/log/apache2/proxy-server + LogLevel debug + CustomLog /var/log/apache2/proxy.log combined + + +Notice that when using Apache the limit on the maximal object size should +be imposed by Apache using the LimitRequestBody rather by the swift proxy. +Note also that the LimitRequestBody should indicate the same value +as indicated by max_file_size located in both +/etc/swift/swift.conf and in /etc/swift/test.conf. +The Swift default value for max_file_size (when not present) is 5368709122. +For example an Apache2 serving as a web front end of a storage node:: + + #Object Service + NameVirtualHost *:6000 + Listen 6000 + + ServerName object-server + WSGIDaemonProcess object-server processes=5 threads=1 + WSGIProcessGroup object-server + WSGIScriptAlias / /var/www/swift/object-server.wsgi + LimitRequestFields 200 + ErrorLog /var/log/apache2/object-server + LogLevel debug + CustomLog /var/log/apache2/access.log combined + + + #Container Service + NameVirtualHost *:6001 + Listen 6001 + + ServerName container-server + WSGIDaemonProcess container-server processes=5 threads=1 + WSGIProcessGroup container-server + WSGIScriptAlias / /var/www/swift/container-server.wsgi + LimitRequestFields 200 + ErrorLog /var/log/apache2/container-server + LogLevel debug + CustomLog /var/log/apache2/access.log combined + + + #Account Service + NameVirtualHost *:6002 + Listen 6002 + + ServerName account-server + WSGIDaemonProcess account-server processes=5 threads=1 + WSGIProcessGroup account-server + WSGIScriptAlias / /var/www/swift/account-server.wsgi + LimitRequestFields 200 + ErrorLog /var/log/apache2/account-server + LogLevel debug + CustomLog /var/log/apache2/access.log combined + + +Next stop the Apache2 and start it again (apache2ctl restart is not enough):: + + apache2ctl stop + apache2ctl start + +Edit the tests config file and add:: + + web_front_end = apache2 + normalized_urls = True + +Also check to see that the file includes max_file_size of the same value as +used for the LimitRequestBody in the apache config file above. + +We are done. +You may run functional tests to test - e.g.:: + + cd ~swift/swift + ./.functests diff --git a/doc/source/deployment_guide.rst b/doc/source/deployment_guide.rst index ab35c54224..8c6191f4eb 100644 --- a/doc/source/deployment_guide.rst +++ b/doc/source/deployment_guide.rst @@ -51,6 +51,15 @@ Load balancing and network design is left as an exercise to the reader, but this is a very important part of the cluster, so time should be spent designing the network for a Swift cluster. + +--------------------- +Web Front End Options +--------------------- + +Swift comes with an integral web front end. However, it can also be deployed +as a request processor of an Apache2 using mod_wsgi as described in +:doc:`Apache Deployment Guide `. + .. _ring-preparing: ------------------ diff --git a/test/functional/tests.py b/test/functional/tests.py index 4b26f85d9f..d6f8d70bc3 100644 --- a/test/functional/tests.py +++ b/test/functional/tests.py @@ -67,6 +67,8 @@ for k in default_constraints: # tests. config[k] = '%s constraint is not defined' % k +web_front_end = config.get('web_front_end', 'integral') +normalized_urls = config.get('normalized_urls', False) def load_constraint(name): c = config[name] @@ -220,7 +222,10 @@ class TestAccount(Base): def testInvalidPath(self): was_url = self.env.account.conn.storage_url - self.env.account.conn.storage_url = "/%s" % was_url + if (normalized_urls): + self.env.account.conn.storage_url = '/' + else: + self.env.account.conn.storage_url = "/%s" % was_url self.env.account.conn.make_request('GET') try: self.assert_status(404) @@ -724,6 +729,7 @@ class TestContainerPathsEnv: 'dir1/subdir+with{whatever/file D', ] + stored_files = set() for f in cls.files: file = cls.container.file(f) if f.endswith('/'): @@ -731,6 +737,16 @@ class TestContainerPathsEnv: else: file.write_random(cls.file_size, hdrs={'Content-Type': 'application/directory'}) + if (normalized_urls): + nfile = '/'.join(filter(None, f.split('/'))) + if (f[-1] == '/'): + nfile += '/' + stored_files.add(nfile) + else: + stored_files.add(f) + cls.stored_files = sorted(stored_files) + + class TestContainerPaths(Base): @@ -754,7 +770,7 @@ class TestContainerPaths(Base): found_files.append(file) recurse_path('') - for file in self.env.files: + for file in self.env.stored_files: if file.startswith('/'): self.assert_(file not in found_dirs) self.assert_(file not in found_files) @@ -764,10 +780,11 @@ class TestContainerPaths(Base): else: self.assert_(file in found_files) self.assert_(file not in found_dirs) + found_files = [] found_dirs = [] recurse_path('/') - for file in self.env.files: + for file in self.env.stored_files: if not file.startswith('/'): self.assert_(file not in found_dirs) self.assert_(file not in found_files) @@ -785,7 +802,7 @@ class TestContainerPaths(Base): if isinstance(files[0], dict): files = [str(x['name']) for x in files] - self.assertEquals(files, sorted(self.env.files)) + self.assertEquals(files, self.env.stored_files) for format in ('json', 'xml'): for file in self.env.container.files(parms={'format': format}): @@ -799,18 +816,20 @@ class TestContainerPaths(Base): def assert_listing(path, list): files = self.env.container.files(parms={'path': path}) self.assertEquals(sorted(list, cmp=locale.strcoll), files) - - assert_listing('/', ['/dir1/', '/dir2/', '/file1', '/file A']) - assert_listing('/dir1', - ['/dir1/file2', '/dir1/subdir1/', '/dir1/subdir2/']) - assert_listing('/dir1/', - ['/dir1/file2', '/dir1/subdir1/', '/dir1/subdir2/']) - assert_listing('/dir1/subdir1', - ['/dir1/subdir1/subsubdir2/', '/dir1/subdir1/file2', - '/dir1/subdir1/file3', '/dir1/subdir1/file4', - '/dir1/subdir1/subsubdir1/']) - assert_listing('/dir1/subdir2', []) - assert_listing('', ['file1', 'dir1/', 'dir2/']) + if not normalized_urls: + assert_listing('/', ['/dir1/', '/dir2/', '/file1', '/file A']) + assert_listing('/dir1', + ['/dir1/file2', '/dir1/subdir1/', '/dir1/subdir2/']) + assert_listing('/dir1/', + ['/dir1/file2', '/dir1/subdir1/', '/dir1/subdir2/']) + assert_listing('/dir1/subdir1', + ['/dir1/subdir1/subsubdir2/', '/dir1/subdir1/file2', + '/dir1/subdir1/file3', '/dir1/subdir1/file4', + '/dir1/subdir1/subsubdir1/']) + assert_listing('/dir1/subdir2', []) + assert_listing('', ['file1', 'dir1/', 'dir2/']) + else: + assert_listing('', ['file1', 'dir1/', 'dir2/', 'file A']) assert_listing('dir1', ['dir1/file2', 'dir1/subdir1/', 'dir1/subdir2/', 'dir1/subdir with spaces/', 'dir1/subdir+with{whatever/']) @@ -1486,6 +1505,8 @@ class TestFile(Base): self.assertEquals(etag, header_etag) def testChunkedPut(self): + if (web_front_end == 'apache2'): + raise SkipTest() data = File.random_data(10000) etag = File.compute_md5sum(data) diff --git a/test/functionalnosetests/swift_testing.py b/test/functionalnosetests/swift_testing.py index 74e8e1cf55..ea9a88a4e9 100644 --- a/test/functionalnosetests/swift_testing.py +++ b/test/functionalnosetests/swift_testing.py @@ -17,16 +17,17 @@ import errno import os import socket import sys -from httplib import HTTPException from time import sleep from nose import SkipTest from ConfigParser import MissingSectionHeaderError from test import get_config -from swiftclient import get_auth, http_connection +from swiftclient import get_auth, http_connection, HTTPException conf = get_config('func_test') +web_front_end = conf.get('web_front_end', 'integral') +normalized_urls = conf.get('normalized_urls', False) # If no conf was read, we will fall back to old school env vars swift_test_auth = os.environ.get('SWIFT_TEST_AUTH') diff --git a/test/functionalnosetests/test_container.py b/test/functionalnosetests/test_container.py index 6b2f1292d9..dd06260fe7 100755 --- a/test/functionalnosetests/test_container.py +++ b/test/functionalnosetests/test_container.py @@ -24,7 +24,7 @@ from swift.common.constraints import MAX_META_COUNT, MAX_META_NAME_LENGTH, \ MAX_META_OVERALL_SIZE, MAX_META_VALUE_LENGTH from swift_testing import check_response, retry, skip, skip2, skip3, \ - swift_test_user + swift_test_user, web_front_end class TestContainer(unittest.TestCase): @@ -561,8 +561,11 @@ class TestContainer(unittest.TestCase): {'X-Auth-Token': token}) return check_response(conn) resp = retry(put) - self.assertEquals(resp.read(), 'Invalid UTF8 or contains NULL') - self.assertEquals(resp.status, 412) + if (web_front_end == 'apache2'): + self.assertEquals(resp.status, 404) + else: + self.assertEquals(resp.read(), 'Invalid UTF8 or contains NULL') + self.assertEquals(resp.status, 412) if __name__ == '__main__': diff --git a/test/functionalnosetests/test_object.py b/test/functionalnosetests/test_object.py index a0981d24e1..82a6a01a03 100755 --- a/test/functionalnosetests/test_object.py +++ b/test/functionalnosetests/test_object.py @@ -22,7 +22,9 @@ from uuid import uuid4 from swift.common.constraints import MAX_META_COUNT, MAX_META_NAME_LENGTH, \ MAX_META_OVERALL_SIZE, MAX_META_VALUE_LENGTH -from swift_testing import check_response, retry, skip, skip3, swift_test_user +from swift_testing import check_response, retry, skip, skip3, \ + swift_test_user, web_front_end +from test import get_config class TestObject(unittest.TestCase): @@ -587,8 +589,11 @@ class TestObject(unittest.TestCase): self.container), 'test', {'X-Auth-Token': token}) return check_response(conn) resp = retry(put) - self.assertEquals(resp.read(), 'Invalid UTF8 or contains NULL') - self.assertEquals(resp.status, 412) + if (web_front_end == 'apache2'): + self.assertEquals(resp.status, 404) + else: + self.assertEquals(resp.read(), 'Invalid UTF8 or contains NULL') + self.assertEquals(resp.status, 412) if __name__ == '__main__':