[pkg-eucalyptus-commits] [SCM] managing cloud instances for Eucalyptus branch, master, updated. 3.0.0-alpha3-257-g1da8e3a

Garrett Holmstrom gholms at fedoraproject.org
Sun Jun 16 02:31:12 UTC 2013


The following commit has been merged in the master branch:
commit 25f140a0934437aed0870d5dcbfbf8883a7d910e
Author: Garrett Holmstrom <gholms at fedoraproject.org>
Date:   Tue Apr 30 11:36:01 2013 -0700

    Port EucaRsaV2Auth to requestbuilder
    
    This commit also updates euca-check-bucket to make it use the NC's auth
    mechanism again.
    
    Fixes TOOLS-269

diff --git a/bin/euca-check-bucket b/bin/euca-check-bucket
index 403a293..28f3578 100755
--- a/bin/euca-check-bucket
+++ b/bin/euca-check-bucket
@@ -1,6 +1,6 @@
 #!/usr/bin/python -tt
 
-import euca2ools.commands.walrus.checkbucket
+import euca2ools.nc.commands.checkbucket
 
 if __name__ == '__main__':
-    euca2ools.commands.walrus.checkbucket.CheckBucket.run()
+    euca2ools.nc.commands.checkbucket.CheckBucket.run()
diff --git a/euca2ools/commands/walrus/checkbucket.py b/euca2ools/commands/walrus/checkbucket.py
index 71518a8..cfb8306 100644
--- a/euca2ools/commands/walrus/checkbucket.py
+++ b/euca2ools/commands/walrus/checkbucket.py
@@ -29,9 +29,7 @@
 # POSSIBILITY OF SUCH DAMAGE.
 
 from euca2ools.commands.walrus import WalrusRequest
-from euca2ools.exceptions import AWSError
 from requestbuilder import Arg
-from requestbuilder.exceptions import ServerError
 
 
 class CheckBucket(WalrusRequest):
diff --git a/euca2ools/nc/__init__.py b/euca2ools/nc/__init__.py
index 839d607..751289e 100644
--- a/euca2ools/nc/__init__.py
+++ b/euca2ools/nc/__init__.py
@@ -1,6 +1,6 @@
 # Software License Agreement (BSD License)
 #
-# Copyright (c) 2009-2011, Eucalyptus Systems, Inc.
+# Copyright (c) 2009-2013, Eucalyptus Systems, Inc.
 # All rights reserved.
 #
 # Redistribution and use of this software in source and binary forms, with or
@@ -27,6 +27,3 @@
 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
-#
-# Author: Mitch Garnaat mgarnaat at eucalyptus.com
-
diff --git a/euca2ools/nc/auth.py b/euca2ools/nc/auth.py
index 4a4f398..b17342b 100644
--- a/euca2ools/nc/auth.py
+++ b/euca2ools/nc/auth.py
@@ -1,6 +1,6 @@
 # Software License Agreement (BSD License)
 #
-# Copyright (c) 2009-2011, Eucalyptus Systems, Inc.
+# Copyright (c) 2009-2013, Eucalyptus Systems, Inc.
 # All rights reserved.
 #
 # Redistribution and use of this software in source and binary forms, with or
@@ -27,146 +27,128 @@
 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
-#
-# Author: Mitch Garnaat mgarnaat at eucalyptus.com
 
-import M2Crypto
+import argparse
 import base64
-import boto.auth_handler
 import datetime
 import hashlib
-import hmac
-import time
+import os.path
+from requestbuilder import Arg
+from requestbuilder.auth import BaseAuth
+from requestbuilder.exceptions import ArgumentError
+import subprocess
+import urlparse
 import urllib
-import warnings
-from boto.exception import BotoClientError
-
-class EucaRsaAuthV1Handler(boto.auth_handler.AuthHandler):
-    """Provides Eucalyptus NC Authentication."""
-
-    capability = ['euca-rsa-v1', 'euca-nc']
-
-    def __init__(self, host, config, provider):
-        boto.auth_handler.AuthHandler.__init__(self, host, config, provider)
-        self.hmac = hmac.new(provider.secret_key, digestmod=hashlib.sha1)
-        self.private_key_path = None
-
-    def _calc_signature(self, params, headers, verb, path):
-        boto.log.debug('using euca_signature')
-        string_to_sign = '%s\n%s\n%s\n' % (verb, headers['Date'], path)
-        keys = params.keys()
-        keys.sort()
-        pairs = []
-        for key in keys:
-            val = params[key]
-            pairs.append(urllib.quote(key, safe='') + '=' + urllib.quote(val, safe='-_~'))
-        qs = '&'.join(pairs)
-        boto.log.debug('query string: %s' % qs)
-        string_to_sign += qs
-        hmac = self.hmac.copy()
-        hmac.update(string_to_sign)
-        sha_manifest = hashlib.sha1()
-        sha_manifest.update(string_to_sign)
-        private_key = M2Crypto.RSA.load_key(self.private_key_path)
-        signature_value = private_key.sign(sha_manifest.digest())
-        b64 = base64.b64encode(signature_value)
-        boto.log.debug('len(b64)=%d' % len(b64))
-        boto.log.debug('base64 encoded digest: %s' % b64)
-        return (qs, b64)
-
-    def add_auth(self, http_request, **kwargs):
-        headers = http_request.headers
-        params = http_request.params
-        qs, signature = self._calc_signature(http_request.params,
-                                             http_request.headers,
-                                             http_request.method,
-                                             http_request.path)
-        headers['EucaSignature'] = signature
-        boto.log.debug('query_string: %s Signature: %s' % (qs, signature))
-        if http_request.method == 'POST':
-            headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
-            http_request.body = qs
-        else:
-            http_request.body = ''
-
-
-class EucaNCAuthHandler(EucaRsaAuthV1Handler):
-    # For API compatibility
-
-    def __init__(self, host, config, provider):
-        warnings.warn(('EucaNCAuthHandler has been renamed to '
-                       'EucaRsaAuthV1Handler'), DeprecationWarning)
-        EucaRsaAuthV1Handler.__init__(self, host, config, provider)
-
-
-class EucaRsaAuthV2Handler(boto.auth_handler.AuthHandler):
-    '''Provides authentication for inter-component requests'''
 
-    capability = ['euca-rsa-v2']
 
-    def __init__(self, host, config, provider):
-        boto.auth_handler.AuthHandler.__init__(self, host, config, provider)
-        self.cert_path        = None
-        self.private_key_path = None
+class EucaRsaV2Auth(BaseAuth):
+    '''Provides authentication for inter-component requests'''
 
-    def add_auth(self, http_request, **kwargs):
-        if 'Authorization' in http_request.headers:
-            del http_request.headers['Authorization']
+    ARGS = [Arg('--cert', metavar='FILE', help='''file containing the X.509
+                certificate to use when signing requests'''),
+            Arg('--privatekey', metavar='FILE',
+                help='file containing the private key to sign requests with'),
+            Arg('--euca-auth', action='store_true', help=argparse.SUPPRESS)]
+
+    def configure(self):
+        BaseAuth.configure(self)
+        cert = self.args.get('cert') or os.getenv('EUCA_CERT')
+        privkey = self.args.get('privatekey') or os.getenv('EUCA_PRIVATE_KEY')
+        if not cert:
+            raise ArgumentError('argument --cert or environment variable '
+                                'EUCA_CERT is required')
+        if not privkey:
+            raise ArgumentError('argument --privatekey or environment '
+                                'variable EUCA_PRIVATE_KEY is required')
+        cert = os.path.expanduser(os.path.expandvars(cert))
+        privkey = os.path.expanduser(os.path.expandvars(privkey))
+        if not os.path.exists(cert):
+            raise ArgumentError("certificate file '{0}' does not exist"
+                                .format(cert))
+        if not os.path.isfile(cert):
+            raise ArgumentError("certificate file '{0}' is not a file"
+                                .format(cert))
+        if not os.path.exists(privkey):
+            raise ArgumentError("private key file '{0}' does not exist"
+                                .format(privkey))
+        if not os.path.isfile(privkey):
+            raise ArgumentError("private key file '{0}' is not a file"
+                                .format(privkey))
+        self.args['cert'] = cert
+        self.args['privatekey'] = privkey
+
+    def __call__(self, request):
+        if request.headers is None:
+            request.headers = {}
         now = datetime.datetime.utcnow()
-        http_request.headers['Date'] = now.strftime('%Y%m%dT%H%M%SZ')
+        request.headers['Date'] = now.strftime('%Y%m%dT%H%M%SZ')
+        if 'Authorization' in request.headers:
+            del request.headers['Authorization']
 
         cert_fp = self._get_fingerprint()
+        self.log.debug('certificate fingerprint: %s', cert_fp)
 
-        headers_to_sign = self._get_headers_to_sign(http_request)
+        headers_to_sign = self._get_headers_to_sign(request)
         signed_headers  = self._get_signed_headers(headers_to_sign)
-        boto.log.debug('SignedHeaders:%s', signed_headers)
+        self.log.debug('SignedHeaders:%s', signed_headers)
 
-        canonical_request = self._get_canonical_request(http_request)
-        boto.log.debug('CanonicalRequest:\n%s', canonical_request)
+        canonical_request = self._get_canonical_request(request)
+        self.log.debug('CanonicalRequest:\n%s', canonical_request)
         signature = self._sign(canonical_request)
-        boto.log.debug('Signature:%s', signature)
+        self.log.debug('Signature:%s', signature)
 
         auth_header = ' '.join(('EUCA2-RSA-SHA256', cert_fp, signed_headers,
                                 signature))
-        http_request.headers['Authorization'] = auth_header
+        request.headers['Authorization'] = auth_header
 
     def _get_fingerprint(self):
-        cert = M2Crypto.X509.load_cert(self.cert_path)
-        return cert.get_fingerprint().lower()
+        cmd = ['openssl', 'x509', '-noout', '-in', self.args['cert'],
+               '-fingerprint', '-md5']
+        openssl = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+        (stdout, __) = openssl.communicate()
+        if openssl.returncode != 0:
+            raise subprocess.CalledProcessError(openssl.returncode, cmd)
+        return stdout.strip().rsplit('=', 1)[-1].replace(':', '').lower()
 
     def _sign(self, canonical_request):
-        privkey = M2Crypto.RSA.load_key(self.private_key_path)
-        digest  = hashlib.sha256(canonical_request).digest()
-        return base64.b64encode(privkey.sign(digest, algo='sha256'))
-
-    def _get_canonical_request(self, http_request):
+        digest = hashlib.sha256(canonical_request).digest()
+        cmd = ['openssl', 'pkeyutl', '-sign', '-inkey',
+               self.args['privatekey'], '-pkeyopt', 'digest:sha256']
+        openssl = subprocess.Popen(cmd, stdin=subprocess.PIPE,
+                                   stdout=subprocess.PIPE)
+        (stdout, __) = openssl.communicate(digest)
+        if openssl.returncode != 0:
+            raise subprocess.CalledProcessError(openssl.returncode, cmd)
+        return base64.b64encode(stdout)
+
+    def _get_canonical_request(self, request):
         # 1.  request method
-        method = http_request.method.upper()
+        method = request.method.upper()
         # 2.  CanonicalURI
-        c_uri  = self._get_canonical_uri(http_request)
+        c_uri  = self._get_canonical_uri(request)
         # 3.  CanonicalQueryString
-        c_querystr = self._get_canonical_querystr(http_request)
+        c_querystr = self._get_canonical_querystr(request)
         # 4.  CanonicalHeaders
-        headers_to_sign = self._get_headers_to_sign(http_request)
+        headers_to_sign = self._get_headers_to_sign(request)
         c_headers = self._get_canonical_headers(headers_to_sign)
         # 5.  SignedHeaders
         s_headers = self._get_signed_headers(headers_to_sign)
 
         return '\n'.join((method, c_uri, c_querystr, c_headers, s_headers))
 
-    def _get_canonical_uri(self, http_request):
-        return http_request.path or '/'
+    def _get_canonical_uri(self, request):
+        return urlparse.urlparse(request.url).path or '/'
 
-    def _get_canonical_querystr(self, http_request):
+    def _get_canonical_querystr(self, request):
         params = []
-        for key, val in http_request.params.iteritems():
-            params.append(urllib.quote(param,    safe='/~') + '=' +
-                          urllib.quote(str(val), safe='~'))
+        for key, val in request.params.iteritems():
+            params.append('='.join((urllib.quote(key, safe='/~'),
+                                    urllib.quote(str(val), safe='~'))))
         return '&'.join(sorted(params))
 
-    def _get_headers_to_sign(self, http_request):
-        headers = {'Host': http_request.host}
-        for key, val in http_request.headers.iteritems():
+    def _get_headers_to_sign(self, request):
+        headers = {'Host': urlparse.urlparse(request.url).netloc}
+        for key, val in request.headers.iteritems():
             if key.lower() != 'authorization':
                 headers[key] = val
         return headers
diff --git a/euca2ools/nc/__init__.py b/euca2ools/nc/commands/__init__.py
similarity index 93%
copy from euca2ools/nc/__init__.py
copy to euca2ools/nc/commands/__init__.py
index 839d607..505e8cd 100644
--- a/euca2ools/nc/__init__.py
+++ b/euca2ools/nc/commands/__init__.py
@@ -1,6 +1,6 @@
 # Software License Agreement (BSD License)
 #
-# Copyright (c) 2009-2011, Eucalyptus Systems, Inc.
+# Copyright (c) 2013, Eucalyptus Systems, Inc.
 # All rights reserved.
 #
 # Redistribution and use of this software in source and binary forms, with or
@@ -27,6 +27,3 @@
 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
-#
-# Author: Mitch Garnaat mgarnaat at eucalyptus.com
-
diff --git a/euca2ools/commands/monitoring/deletealarms.py b/euca2ools/nc/commands/checkbucket.py
similarity index 76%
copy from euca2ools/commands/monitoring/deletealarms.py
copy to euca2ools/nc/commands/checkbucket.py
index a86d2ef..864d388 100644
--- a/euca2ools/commands/monitoring/deletealarms.py
+++ b/euca2ools/nc/commands/checkbucket.py
@@ -29,13 +29,14 @@
 # POSSIBILITY OF SUCH DAMAGE.
 
 import argparse
-from euca2ools.commands.monitoring import CloudWatchRequest
+import euca2ools.commands.walrus.checkbucket
+import euca2ools.nc.services
 from requestbuilder import Arg
 
 
-class DeleteAlarms(CloudWatchRequest):
-    DESCRIPTION = 'Delete alarms'
-    ARGS = [Arg('AlarmNames.member', metavar='ALARM', nargs='+',
-                help='names of the alarms to delete'),
-            Arg('-f', '--force', action='store_true', route_to=None,
-                help=argparse.SUPPRESS)]  # for compatibility
+class CheckBucket(euca2ools.commands.walrus.checkbucket.CheckBucket):
+    DESCRIPTION = ('[Eucalyptus NC internal] Return successfully if a bucket '
+                   'already exists')
+    SERVICE_CLASS = euca2ools.nc.services.NCInternalWalrus
+    ARGS = [Arg('-b', action='store_true', route_to=None,
+                help=argparse.SUPPRESS)]  # This makes the -b optional
diff --git a/euca2ools/nc/connection.py b/euca2ools/nc/connection.py
deleted file mode 100644
index 0300fff..0000000
--- a/euca2ools/nc/connection.py
+++ /dev/null
@@ -1,179 +0,0 @@
-# Software License Agreement (BSD License)
-#
-# Copyright (c) 2009-2011, Eucalyptus Systems, Inc.
-# All rights reserved.
-#
-# Redistribution and use of this software in source and binary forms, with or
-# without modification, are permitted provided that the following conditions
-# are met:
-#
-#   Redistributions of source code must retain the above
-#   copyright notice, this list of conditions and the
-#   following disclaimer.
-#
-#   Redistributions in binary form must reproduce the above
-#   copyright notice, this list of conditions and the
-#   following disclaimer in the documentation and/or other
-#   materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
-# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-# POSSIBILITY OF SUCH DAMAGE.
-#
-# Author: Mitch Garnaat mgarnaat at eucalyptus.com
-
-import base64
-import binascii
-import boto
-import hashlib
-import time
-import urllib
-from boto.connection import AWSAuthConnection
-from boto.exception import BotoClientError, S3ResponseError, S3CreateError
-from boto.resultset import ResultSet
-from boto.s3.bucket import Bucket
-
-class EucaConnection(AWSAuthConnection):
-
-    DefaultHost = 'localhost'
-    DefaultPort = 8773
-
-    def __init__(self, private_key_path, cert_path,
-                 aws_access_key_id=None, aws_secret_access_key=None,
-                 is_secure=False, port=DefaultPort, proxy=None, proxy_port=None,
-                 proxy_user=None, proxy_pass=None,
-                 host=DefaultHost, debug=0, https_connection_factory=None,
-                 path='/'):
-        self.private_key_path = private_key_path
-        self.cert_path = cert_path
-        AWSAuthConnection.__init__(self, host, aws_access_key_id,
-                                   aws_secret_access_key,
-                                   is_secure, port, proxy, proxy_port,
-                                   proxy_user, proxy_pass,
-                                   debug=debug,
-                                   https_connection_factory=https_connection_factory,
-                                   path=path)
-        self._auth_handler.cert_path        = self.cert_path
-        self._auth_handler.private_key_path = self.private_key_path
-
-    def _required_auth_capability(self):
-        return ['euca-rsa-v2']
-
-    def make_request(self, method='GET', bucket='', key='', headers=None,
-                     data='', query_args=None, sender=None,
-                     override_num_retries=None, action=None,
-                     effective_user_id = None, params=None):
-        if headers is None:
-            headers = {}
-        if params is None:
-            params = {}
-        if not effective_user_id:
-            effective_user_id = self.aws_access_key_id
-        if action:
-            headers['EucaOperation'] = action
-        headers['AWSAccessKeyId'] = effective_user_id
-        cert_file = open(self.cert_path, 'r')
-        cert_str = cert_file.read()
-        cert_file.close()
-        if not 'Content-Length' in headers:
-            headers['Content-Length'] = str(len(data))
-        if not 'Content-MD5' in headers:
-            md5sum = hashlib.md5(data).digest()
-            headers['Content-MD5'] = binascii.hexlify(md5sum)
-        utf8_params = {}
-        for key in params:
-            utf8_params[key] = self.get_utf8_value(params[key])
-        path_base = '/'
-        path_base += "%s/" % bucket
-        path = path_base + urllib.quote(key)
-        http_request = self.build_base_http_request(method, path, None,
-                                                    utf8_params,
-                                                    headers, data,
-                                                    self.server_name())
-        # Use EUCA2 signing
-        response = self._mexe(http_request, sender)
-        if response.status == 403:
-            # Use EUCA signing in case we're talking to older Eucalyptus
-            headers['EucaEffectiveUserId'] = effective_user_id
-            headers['EucaCert'] = base64.b64encode(cert_str)
-            headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT",
-                                            time.gmtime())
-            http_request = self.build_base_http_request(method, path, None,
-                                                        utf8_params,
-                                                        headers, data,
-                                                        self.server_name())
-            self._auth_handler = boto.auth.get_auth_handler(self.host,
-                                                            boto.config,
-                                                            self.provider,
-                                                            ['euca-rsa-v1'])
-            self._auth_handler.private_key_path = self.private_key_path
-            response = self._mexe(http_request, sender)
-        return response
-
-    def get_bucket(self, bucket_name, validate=True, headers=None):
-        bucket = Bucket(self, bucket_name)
-        if validate:
-            bucket.get_all_keys(headers, maxkeys=0)
-        return bucket
-
-    def create_bucket(self, bucket_name, headers=None,
-                      location='', policy=None):
-        """
-        Creates a new located bucket. By default it's in the USA. You can pass
-        Location.EU to create an European bucket.
-
-        :type bucket_name: string
-        :param bucket_name: The name of the new bucket
-        
-        :type headers: dict
-        :param headers: Additional headers to pass along with
-                        the request to AWS.
-
-        :type location: :class:`boto.s3.connection.Location`
-        :param location: The location of the new bucket
-        
-        :type policy: :class:`boto.s3.acl.CannedACLStrings`
-        :param policy: A canned ACL policy that will be applied
-                       to the new key in S3.
-             
-        """
-        if not bucket_name.islower():
-            raise BotoClientError("Bucket names must be lower case.")
-
-        if policy:
-            if headers:
-                headers['x-amz-acl'] = policy
-            else:
-                headers = {'x-amz-acl' : policy}
-        if location == '':
-            data = ''
-        else:
-            data = '<CreateBucketConstraint><LocationConstraint>' + \
-                    location + '</LocationConstraint></CreateBucketConstraint>'
-        response = self.make_request('PUT', bucket_name, headers=headers,
-                data=data)
-        body = response.read()
-        if response.status == 409:
-            raise S3CreateError(response.status, response.reason, body)
-        if response.status == 200:
-            return Bucket(self, bucket_name)
-        else:
-            raise self.provider.storage_response_error(
-                response.status, response.reason, body)
-
-    def delete_bucket(self, bucket, headers=None):
-        response = self.make_request('DELETE', bucket, headers=headers)
-        body = response.read()
-        if response.status != 204:
-            raise self.provider.storage_response_error(
-                response.status, response.reason, body)
-
-
diff --git a/bin/euca-version b/euca2ools/nc/services.py
old mode 100755
new mode 100644
similarity index 86%
copy from bin/euca-version
copy to euca2ools/nc/services.py
index 33bbc34..672da94
--- a/bin/euca-version
+++ b/euca2ools/nc/services.py
@@ -1,8 +1,6 @@
-#!/usr/bin/python -tt
-
 # Software License Agreement (BSD License)
 #
-# Copyright (c) 2009-2013, Eucalyptus Systems, Inc.
+# Copyright (c) 2013, Eucalyptus Systems, Inc.
 # All rights reserved.
 #
 # Redistribution and use of this software in source and binary forms, with or
@@ -30,7 +28,9 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-import euca2ools.commands
-import sys
+import euca2ools.nc.auth
+import euca2ools.commands.walrus
+
 
-print >> sys.stderr, euca2ools.commands.Euca2ools().format_version()
+class NCInternalWalrus(euca2ools.commands.walrus.Walrus):
+    AUTH_CLASS = euca2ools.nc.auth.EucaRsaV2Auth

-- 
managing cloud instances for Eucalyptus



More information about the pkg-eucalyptus-commits mailing list