Merge lp:~rconradharris/glance/lp615675 into lp:~hudson-openstack/glance/trunk

Proposed by Rick Harris
Status: Merged
Approved by: Christopher MacGown
Approved revision: 12
Merged at revision: 8
Proposed branch: lp:~rconradharris/glance/lp615675
Merge into: lp:~hudson-openstack/glance/trunk
Diff against target: 1755 lines (+1655/-16)
16 files modified
bin/parallax-server.py (+51/-0)
glance/common/db/__init__.py (+23/-0)
glance/common/db/api.py (+88/-0)
glance/common/db/sqlalchemy/__init__.py (+24/-0)
glance/common/db/sqlalchemy/api.py (+127/-0)
glance/common/db/sqlalchemy/models.py (+191/-0)
glance/common/db/sqlalchemy/session.py (+42/-0)
glance/common/exception.py (+87/-0)
glance/common/flags.py (+175/-0)
glance/common/server.py (+144/-0)
glance/common/utils.py (+204/-0)
glance/common/wsgi.py (+298/-0)
glance/parallax/__init__.py (+21/-16)
glance/parallax/api/__init__.py (+49/-0)
glance/parallax/api/images.py (+82/-0)
tests/test_data.py (+49/-0)
To merge this branch: bzr merge lp:~rconradharris/glance/lp615675
Reviewer Review Type Date Requested Status
Christopher MacGown (community) Approve
Review via email: mp+37024@code.launchpad.net

Description of the change

This patch:
  * pulls in a number of useful libraries from Nova under the common/ path (we can factor those out to a shared library in Bexar-release)
  * Defines the models in common.db.sqlalchemy.models.py (this should be factored out into the parallax package soon)
  * Adds the parallax api-server under /bin (if PyPI was used to pull python-daemon and python-lockfile, you may need to apply a patch I have against it)

To post a comment you must log in.
Revision history for this message
Christopher MacGown (0x44) wrote :

lgtm

review: Approve
Revision history for this message
Christopher MacGown (0x44) wrote :

Actually, hold that. In the below, any instance of chunk_dicts and chunk should be replaced by file, since they're the files that make up an image. Teller uses the term 'chunk' to mean an arbitrary sized piece of the read file buffer yielded to the requester.

82 + chunk_dicts = []
1683 + for chunk in image.image_chunks:
1684 + chunk_dict = dict(location=chunk.location, size=chunk.size)
1685 + chunk_dicts.append(chunk_dict)
1686 +
1687 + metadata_dicts = []
1688 + for metadatum in image.image_metadata:
1689 + metadatum_dict = dict(key=metadatum.key, value=metadatum.value)
1690 + metadata_dicts.append(metadatum_dict)
1691 +
1692 + image_dict = dict(id=image.id, name=image.name, state=image.state,
1693 + public=image.public, chunks=chunk_dicts,

review: Needs Fixing
Revision history for this message
Jay Pipes (jaypipes) wrote :

Looks good! We can refactor the common stuff into openstack-common after Austin, of course.

The only suggestion I have is to combine the glance.parallax.api.API and glance.parallax.api.APIRouter class into a single class. wsgi.Router *is* middleware (since its constructor sets routes.RoutesMiddleware as the application to return, and its __call__ method returns that application, so unless I'm mistaken, the following code should work fine:

class API(wsgi.Router):
    """
    Routes requests on the Parallax to the appropriate controller
    and method.
    """

    def __init__(self):
        mapper = routes.Mapper()
        mapper.resource("image", "images", controller=images.Controller(),
            collection={'detail': 'GET'})
        super(APIRouter, self).__init__(mapper)

Cheers!
jay

Revision history for this message
Jay Pipes (jaypipes) wrote :

s/super(APIRouter/super(API/

in the code above.

lp:~rconradharris/glance/lp615675 updated
12. By Rick Harris

ImageChunk -> ImageFile, merging APIRouter into API for now

Revision history for this message
Christopher MacGown (0x44) wrote :

Awesome.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'bin/parallax-server.py'
--- bin/parallax-server.py 1970-01-01 00:00:00 +0000
+++ bin/parallax-server.py 2010-09-29 20:56:47 +0000
@@ -0,0 +1,51 @@
1#!/usr/bin/env python
2# pylint: disable-msg=C0103
3# vim: tabstop=4 shiftwidth=4 softtabstop=4
4
5# Copyright 2010 United States Government as represented by the
6# Administrator of the National Aeronautics and Space Administration.
7# All Rights Reserved.
8#
9# Licensed under the Apache License, Version 2.0 (the "License");
10# you may not use this file except in compliance with the License.
11# You may obtain a copy of the License at
12#
13# http://www.apache.org/licenses/LICENSE-2.0
14#
15# Unless required by applicable law or agreed to in writing, software
16# distributed under the License is distributed on an "AS IS" BASIS,
17# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18# See the License for the specific language governing permissions and
19# limitations under the License.
20"""
21Parallax API daemon.
22"""
23
24import os
25import sys
26
27# If ../parallax/__init__.py exists, add ../ to Python search path, so that
28# it will override what happens to be installed in /usr/(local/)lib/python...
29possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
30 os.pardir,
31 os.pardir))
32if os.path.exists(os.path.join(possible_topdir, 'parallax', '__init__.py')):
33 sys.path.insert(0, possible_topdir)
34
35from glance.common import flags
36from glance.common import utils
37from glance.common import server
38from glance.common import wsgi
39from glance.parallax import api
40
41
42FLAGS = flags.FLAGS
43# TODO(sirp): ensure no conflicts in port selection
44flags.DEFINE_integer('parallax_port', 9191, 'Parallax port')
45
46def main(_args):
47 wsgi.run_server(api.API(), FLAGS.parallax_port)
48
49if __name__ == '__main__':
50 utils.default_flagfile()
51 server.serve('parallax-server', main)
052
=== added directory 'glance/common'
=== added file 'glance/common/__init__.py'
=== added directory 'glance/common/db'
=== added file 'glance/common/db/__init__.py'
--- glance/common/db/__init__.py 1970-01-01 00:00:00 +0000
+++ glance/common/db/__init__.py 2010-09-29 20:56:47 +0000
@@ -0,0 +1,23 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
4# Copyright 2010 United States Government as represented by the
5# Administrator of the National Aeronautics and Space Administration.
6# All Rights Reserved.
7#
8# Licensed under the Apache License, Version 2.0 (the "License"); you may
9# not use this file except in compliance with the License. You may obtain
10# a copy of the License at
11#
12# http://www.apache.org/licenses/LICENSE-2.0
13#
14# Unless required by applicable law or agreed to in writing, software
15# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17# License for the specific language governing permissions and limitations
18# under the License.
19"""
20DB abstraction for Nova and Glance
21"""
22
23from glance.common.db.api import *
024
=== added file 'glance/common/db/api.py'
--- glance/common/db/api.py 1970-01-01 00:00:00 +0000
+++ glance/common/db/api.py 2010-09-29 20:56:47 +0000
@@ -0,0 +1,88 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 United States Government as represented by the
4# Administrator of the National Aeronautics and Space Administration.
5# All Rights Reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License. You may obtain
9# a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16# License for the specific language governing permissions and limitations
17# under the License.
18"""
19Defines interface for DB access
20"""
21
22from glance.common import exception
23from glance.common import flags
24from glance.common import utils
25
26
27FLAGS = flags.FLAGS
28flags.DEFINE_string('db_backend', 'sqlalchemy',
29 'The backend to use for db')
30
31
32IMPL = utils.LazyPluggable(FLAGS['db_backend'],
33 sqlalchemy='glance.common.db.sqlalchemy.api')
34
35
36###################
37
38
39def image_create(context, values):
40 """Create an image from the values dictionary."""
41 return IMPL.image_create(context, values)
42
43
44
45def image_destroy(context, image_id):
46 """Destroy the image or raise if it does not exist."""
47 return IMPL.image_destroy(context, image_id)
48
49
50def image_get(context, image_id):
51 """Get an image or raise if it does not exist."""
52 return IMPL.image_get(context, image_id)
53
54
55def image_get_all(context):
56 """Get all images."""
57 return IMPL.image_get_all(context)
58
59
60def image_get_by_str(context, str_id):
61 """Get an image by string id."""
62 return IMPL.image_get_by_str(context, str_id)
63
64
65def image_update(context, image_id, values):
66 """Set the given properties on an image and update it.
67
68 Raises NotFound if image does not exist.
69
70 """
71 return IMPL.image_update(context, image_id, values)
72
73
74###################
75
76
77def image_file_create(context, values):
78 """Create an image file from the values dictionary."""
79 return IMPL.image_file_create(context, values)
80
81
82###################
83
84
85def image_metadatum_create(context, values):
86 """Create an image metadatum from the values dictionary."""
87 return IMPL.image_metadatum_create(context, values)
88
089
=== added directory 'glance/common/db/sqlalchemy'
=== added file 'glance/common/db/sqlalchemy/__init__.py'
--- glance/common/db/sqlalchemy/__init__.py 1970-01-01 00:00:00 +0000
+++ glance/common/db/sqlalchemy/__init__.py 2010-09-29 20:56:47 +0000
@@ -0,0 +1,24 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 United States Government as represented by the
4# Administrator of the National Aeronautics and Space Administration.
5# All Rights Reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License. You may obtain
9# a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16# License for the specific language governing permissions and limitations
17# under the License.
18
19"""
20SQLAlchemy database backend
21"""
22from glance.common.db.sqlalchemy import models
23
24models.register_models()
025
=== added file 'glance/common/db/sqlalchemy/api.py'
--- glance/common/db/sqlalchemy/api.py 1970-01-01 00:00:00 +0000
+++ glance/common/db/sqlalchemy/api.py 2010-09-29 20:56:47 +0000
@@ -0,0 +1,127 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 United States Government as represented by the
4# Administrator of the National Aeronautics and Space Administration.
5# All Rights Reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License. You may obtain
9# a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16# License for the specific language governing permissions and limitations
17# under the License.
18"""
19Implementation of SQLAlchemy backend
20"""
21
22import sys
23from glance.common import db
24from glance.common import exception
25from glance.common import flags
26from glance.common.db.sqlalchemy import models
27from glance.common.db.sqlalchemy.session import get_session
28from sqlalchemy.orm import exc
29
30#from sqlalchemy.orm import joinedload_all
31# TODO(sirp): add back eager loading
32from sqlalchemy.orm import joinedload
33from sqlalchemy.sql import func
34
35FLAGS = flags.FLAGS
36
37
38# NOTE(vish): disabling docstring pylint because the docstrings are
39# in the interface definition
40# pylint: disable-msg=C0111
41def _deleted(context):
42 """Calculates whether to include deleted objects based on context.
43
44 Currently just looks for a flag called deleted in the context dict.
45 """
46 if not hasattr(context, 'get'):
47 return False
48 return context.get('deleted', False)
49
50
51###################
52
53
54def image_create(_context, values):
55 image_ref = models.Image()
56 for (key, value) in values.iteritems():
57 image_ref[key] = value
58 image_ref.save()
59 return image_ref
60
61
62def image_destroy(_context, image_id):
63 session = get_session()
64 with session.begin():
65 image_ref = models.Image.find(image_id, session=session)
66 image_ref.delete(session=session)
67
68
69def image_get(context, image_id):
70 session = get_session()
71 try:
72 return session.query(models.Image
73 ).options(joinedload(models.Image.files)
74 ).options(joinedload(models.Image.metadata)
75 ).filter_by(deleted=_deleted(context)
76 ).filter_by(id=image_id
77 ).one()
78 except exc.NoResultFound:
79 new_exc = exception.NotFound("No model for id %s" % image_id)
80 raise new_exc.__class__, new_exc, sys.exc_info()[2]
81 return models.Image.find(image_id, deleted=_deleted(context))
82
83
84def image_get_all(context):
85 session = get_session()
86 # TODO(sirp): add back eager loading
87 return session.query(models.Image
88 #).options(joinedload(models.Image.files)
89 #).options(joinedload(models.Image.metadata)
90 ).filter_by(deleted=_deleted(context)
91 ).all()
92
93
94def image_get_by_str(context, str_id):
95 return models.Image.find_by_str(str_id, deleted=_deleted(context))
96
97
98def image_update(_context, image_id, values):
99 session = get_session()
100 with session.begin():
101 image_ref = models.Image.find(image_id, session=session)
102 for (key, value) in values.iteritems():
103 image_ref[key] = value
104 image_ref.save(session=session)
105
106
107###################
108
109
110def image_file_create(_context, values):
111 image_file_ref = models.ImageFile()
112 for (key, value) in values.iteritems():
113 image_file_ref[key] = value
114 image_file_ref.save()
115 return image_file_ref
116
117
118###################
119
120
121def image_metadatum_create(_context, values):
122 image_metadatum_ref = models.ImageMetadatum()
123 for (key, value) in values.iteritems():
124 image_metadatum_ref[key] = value
125 image_metadatum_ref.save()
126 return image_metadatum_ref
127
0128
=== added file 'glance/common/db/sqlalchemy/models.py'
--- glance/common/db/sqlalchemy/models.py 1970-01-01 00:00:00 +0000
+++ glance/common/db/sqlalchemy/models.py 2010-09-29 20:56:47 +0000
@@ -0,0 +1,191 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 United States Government as represented by the
4# Administrator of the National Aeronautics and Space Administration.
5# All Rights Reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License. You may obtain
9# a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16# License for the specific language governing permissions and limitations
17# under the License.
18
19"""
20SQLAlchemy models for glance data
21"""
22
23import sys
24import datetime
25
26# TODO(vish): clean up these imports
27from sqlalchemy.orm import relationship, backref, exc, object_mapper
28from sqlalchemy import Column, Integer, String
29from sqlalchemy import ForeignKey, DateTime, Boolean, Text
30from sqlalchemy.ext.declarative import declarative_base
31
32from glance.common.db.sqlalchemy.session import get_session
33
34# FIXME(sirp): confirm this is not needed
35#from common import auth
36from glance.common import exception
37from glance.common import flags
38
39FLAGS = flags.FLAGS
40
41BASE = declarative_base()
42
43#TODO(sirp): ModelBase should be moved out so Glance and Nova can share it
44class ModelBase(object):
45 """Base class for Nova and Glance Models"""
46 __table_args__ = {'mysql_engine': 'InnoDB'}
47 __table_initialized__ = False
48 __prefix__ = 'none'
49 created_at = Column(DateTime, default=datetime.datetime.utcnow)
50 updated_at = Column(DateTime, onupdate=datetime.datetime.utcnow)
51 deleted_at = Column(DateTime)
52 deleted = Column(Boolean, default=False)
53
54 @classmethod
55 def all(cls, session=None, deleted=False):
56 """Get all objects of this type"""
57 if not session:
58 session = get_session()
59 return session.query(cls
60 ).filter_by(deleted=deleted
61 ).all()
62
63 @classmethod
64 def count(cls, session=None, deleted=False):
65 """Count objects of this type"""
66 if not session:
67 session = get_session()
68 return session.query(cls
69 ).filter_by(deleted=deleted
70 ).count()
71
72 @classmethod
73 def find(cls, obj_id, session=None, deleted=False):
74 """Find object by id"""
75 if not session:
76 session = get_session()
77 try:
78 return session.query(cls
79 ).filter_by(id=obj_id
80 ).filter_by(deleted=deleted
81 ).one()
82 except exc.NoResultFound:
83 new_exc = exception.NotFound("No model for id %s" % obj_id)
84 raise new_exc.__class__, new_exc, sys.exc_info()[2]
85
86 @classmethod
87 def find_by_str(cls, str_id, session=None, deleted=False):
88 """Find object by str_id"""
89 int_id = int(str_id.rpartition('-')[2])
90 return cls.find(int_id, session=session, deleted=deleted)
91
92 @property
93 def str_id(self):
94 """Get string id of object (generally prefix + '-' + id)"""
95 return "%s-%s" % (self.__prefix__, self.id)
96
97 def save(self, session=None):
98 """Save this object"""
99 if not session:
100 session = get_session()
101 session.add(self)
102 session.flush()
103
104 def delete(self, session=None):
105 """Delete this object"""
106 self.deleted = True
107 self.deleted_at = datetime.datetime.utcnow()
108 self.save(session=session)
109
110 def __setitem__(self, key, value):
111 setattr(self, key, value)
112
113 def __getitem__(self, key):
114 return getattr(self, key)
115
116 def __iter__(self):
117 self._i = iter(object_mapper(self).columns)
118 return self
119
120 def next(self):
121 n = self._i.next().name
122 return n, getattr(self, n)
123
124class Image(BASE, ModelBase):
125 """Represents an image in the datastore"""
126 __tablename__ = 'images'
127 __prefix__ = 'img'
128
129 id = Column(Integer, primary_key=True)
130 name = Column(String(255))
131 image_type = Column(String(255))
132 state = Column(String(255))
133 public = Column(Boolean, default=False)
134
135 #@validates('image_type')
136 #def validate_image_type(self, key, image_type):
137 # assert(image_type in ('machine', 'kernel', 'ramdisk', 'raw'))
138 #
139 #@validates('state')
140 #def validate_state(self, key, state):
141 # assert(state in ('available', 'pending', 'disabled'))
142 #
143 # TODO(sirp): should these be stored as metadata?
144 #user_id = Column(String(255))
145 #project_id = Column(String(255))
146 #arch = Column(String(255))
147 #default_kernel_id = Column(String(255))
148 #default_ramdisk_id = Column(String(255))
149 #
150 #@validates('default_kernel_id')
151 #def validate_kernel_id(self, key, val):
152 # if val != 'machine':
153 # assert(val is None)
154 #
155 #@validates('default_ramdisk_id')
156 #def validate_ramdisk_id(self, key, val):
157 # if val != 'machine':
158 # assert(val is None)
159
160
161class ImageFile(BASE, ModelBase):
162 """Represents an image file in the datastore"""
163 __tablename__ = 'image_files'
164 __prefix__ = 'img-file'
165 id = Column(Integer, primary_key=True)
166 image_id = Column(Integer, ForeignKey('images.id'), nullable=False)
167 image = relationship(Image, backref=backref('files'))
168
169 location = Column(String(255))
170 size = Column(Integer)
171
172
173class ImageMetadatum(BASE, ModelBase):
174 """Represents an image metadata in the datastore"""
175 __tablename__ = 'image_metadata'
176 __prefix__ = 'mdata'
177 id = Column(Integer, primary_key=True)
178 image_id = Column(Integer, ForeignKey('images.id'), nullable=False)
179 image = relationship(Image, backref=backref('metadata'))
180
181 key = Column(String(255), index=True, unique=True)
182 value = Column(Text)
183
184
185def register_models():
186 """Register Models and create metadata"""
187 from sqlalchemy import create_engine
188 models = (Image, ImageFile, ImageMetadatum)
189 engine = create_engine(FLAGS.sql_connection, echo=False)
190 for model in models:
191 model.metadata.create_all(engine)
0192
=== added file 'glance/common/db/sqlalchemy/session.py'
--- glance/common/db/sqlalchemy/session.py 1970-01-01 00:00:00 +0000
+++ glance/common/db/sqlalchemy/session.py 2010-09-29 20:56:47 +0000
@@ -0,0 +1,42 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 United States Government as represented by the
4# Administrator of the National Aeronautics and Space Administration.
5# All Rights Reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License. You may obtain
9# a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16# License for the specific language governing permissions and limitations
17# under the License.
18"""
19Session Handling for SQLAlchemy backend
20"""
21
22from sqlalchemy import create_engine
23from sqlalchemy.orm import sessionmaker
24
25from glance.common import flags
26
27FLAGS = flags.FLAGS
28
29_ENGINE = None
30_MAKER = None
31
32def get_session(autocommit=True, expire_on_commit=False):
33 """Helper method to grab session"""
34 global _ENGINE
35 global _MAKER
36 if not _MAKER:
37 if not _ENGINE:
38 _ENGINE = create_engine(FLAGS.sql_connection, echo=False)
39 _MAKER = sessionmaker(bind=_ENGINE,
40 autocommit=autocommit,
41 expire_on_commit=expire_on_commit)
42 return _MAKER()
043
=== added file 'glance/common/exception.py'
--- glance/common/exception.py 1970-01-01 00:00:00 +0000
+++ glance/common/exception.py 2010-09-29 20:56:47 +0000
@@ -0,0 +1,87 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 United States Government as represented by the
4# Administrator of the National Aeronautics and Space Administration.
5# All Rights Reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License. You may obtain
9# a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16# License for the specific language governing permissions and limitations
17# under the License.
18
19"""
20Nova base exception handling, including decorator for re-raising
21Nova-type exceptions. SHOULD include dedicated exception logging.
22"""
23
24import logging
25import sys
26import traceback
27
28
29class ProcessExecutionError(IOError):
30 def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None,
31 description=None):
32 if description is None:
33 description = "Unexpected error while running command."
34 if exit_code is None:
35 exit_code = '-'
36 message = "%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % (
37 description, cmd, exit_code, stdout, stderr)
38 IOError.__init__(self, message)
39
40
41class Error(Exception):
42 def __init__(self, message=None):
43 super(Error, self).__init__(message)
44
45
46class ApiError(Error):
47 def __init__(self, message='Unknown', code='Unknown'):
48 self.message = message
49 self.code = code
50 super(ApiError, self).__init__('%s: %s'% (code, message))
51
52
53class NotFound(Error):
54 pass
55
56
57class Duplicate(Error):
58 pass
59
60
61class NotAuthorized(Error):
62 pass
63
64
65class NotEmpty(Error):
66 pass
67
68
69class Invalid(Error):
70 pass
71
72
73def wrap_exception(f):
74 def _wrap(*args, **kw):
75 try:
76 return f(*args, **kw)
77 except Exception, e:
78 if not isinstance(e, Error):
79 #exc_type, exc_value, exc_traceback = sys.exc_info()
80 logging.exception('Uncaught exception')
81 #logging.error(traceback.extract_stack(exc_traceback))
82 raise Error(str(e))
83 raise
84 _wrap.func_name = f.func_name
85 return _wrap
86
87
088
=== added file 'glance/common/flags.py'
--- glance/common/flags.py 1970-01-01 00:00:00 +0000
+++ glance/common/flags.py 2010-09-29 20:56:47 +0000
@@ -0,0 +1,175 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 United States Government as represented by the
4# Administrator of the National Aeronautics and Space Administration.
5# All Rights Reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License. You may obtain
9# a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16# License for the specific language governing permissions and limitations
17# under the License.
18
19"""
20Package-level global flags are defined here, the rest are defined
21where they're used.
22"""
23
24import getopt
25import os
26import socket
27import sys
28
29import gflags
30
31
32class FlagValues(gflags.FlagValues):
33 """Extension of gflags.FlagValues that allows undefined and runtime flags.
34
35 Unknown flags will be ignored when parsing the command line, but the
36 command line will be kept so that it can be replayed if new flags are
37 defined after the initial parsing.
38
39 """
40
41 def __init__(self):
42 gflags.FlagValues.__init__(self)
43 self.__dict__['__dirty'] = []
44 self.__dict__['__was_already_parsed'] = False
45 self.__dict__['__stored_argv'] = []
46
47 def __call__(self, argv):
48 # We're doing some hacky stuff here so that we don't have to copy
49 # out all the code of the original verbatim and then tweak a few lines.
50 # We're hijacking the output of getopt so we can still return the
51 # leftover args at the end
52 sneaky_unparsed_args = {"value": None}
53 original_argv = list(argv)
54
55 if self.IsGnuGetOpt():
56 orig_getopt = getattr(getopt, 'gnu_getopt')
57 orig_name = 'gnu_getopt'
58 else:
59 orig_getopt = getattr(getopt, 'getopt')
60 orig_name = 'getopt'
61
62 def _sneaky(*args, **kw):
63 optlist, unparsed_args = orig_getopt(*args, **kw)
64 sneaky_unparsed_args['value'] = unparsed_args
65 return optlist, unparsed_args
66
67 try:
68 setattr(getopt, orig_name, _sneaky)
69 args = gflags.FlagValues.__call__(self, argv)
70 except gflags.UnrecognizedFlagError:
71 # Undefined args were found, for now we don't care so just
72 # act like everything went well
73 # (these three lines are copied pretty much verbatim from the end
74 # of the __call__ function we are wrapping)
75 unparsed_args = sneaky_unparsed_args['value']
76 if unparsed_args:
77 if self.IsGnuGetOpt():
78 args = argv[:1] + unparsed_args
79 else:
80 args = argv[:1] + original_argv[-len(unparsed_args):]
81 else:
82 args = argv[:1]
83 finally:
84 setattr(getopt, orig_name, orig_getopt)
85
86 # Store the arguments for later, we'll need them for new flags
87 # added at runtime
88 self.__dict__['__stored_argv'] = original_argv
89 self.__dict__['__was_already_parsed'] = True
90 self.ClearDirty()
91 return args
92
93 def SetDirty(self, name):
94 """Mark a flag as dirty so that accessing it will case a reparse."""
95 self.__dict__['__dirty'].append(name)
96
97 def IsDirty(self, name):
98 return name in self.__dict__['__dirty']
99
100 def ClearDirty(self):
101 self.__dict__['__is_dirty'] = []
102
103 def WasAlreadyParsed(self):
104 return self.__dict__['__was_already_parsed']
105
106 def ParseNewFlags(self):
107 if '__stored_argv' not in self.__dict__:
108 return
109 new_flags = FlagValues()
110 for k in self.__dict__['__dirty']:
111 new_flags[k] = gflags.FlagValues.__getitem__(self, k)
112
113 new_flags(self.__dict__['__stored_argv'])
114 for k in self.__dict__['__dirty']:
115 setattr(self, k, getattr(new_flags, k))
116 self.ClearDirty()
117
118 def __setitem__(self, name, flag):
119 gflags.FlagValues.__setitem__(self, name, flag)
120 if self.WasAlreadyParsed():
121 self.SetDirty(name)
122
123 def __getitem__(self, name):
124 if self.IsDirty(name):
125 self.ParseNewFlags()
126 return gflags.FlagValues.__getitem__(self, name)
127
128 def __getattr__(self, name):
129 if self.IsDirty(name):
130 self.ParseNewFlags()
131 return gflags.FlagValues.__getattr__(self, name)
132
133
134FLAGS = FlagValues()
135
136
137def _wrapper(func):
138 def _wrapped(*args, **kw):
139 kw.setdefault('flag_values', FLAGS)
140 func(*args, **kw)
141 _wrapped.func_name = func.func_name
142 return _wrapped
143
144
145DEFINE = _wrapper(gflags.DEFINE)
146DEFINE_string = _wrapper(gflags.DEFINE_string)
147DEFINE_integer = _wrapper(gflags.DEFINE_integer)
148DEFINE_bool = _wrapper(gflags.DEFINE_bool)
149DEFINE_boolean = _wrapper(gflags.DEFINE_boolean)
150DEFINE_float = _wrapper(gflags.DEFINE_float)
151DEFINE_enum = _wrapper(gflags.DEFINE_enum)
152DEFINE_list = _wrapper(gflags.DEFINE_list)
153DEFINE_spaceseplist = _wrapper(gflags.DEFINE_spaceseplist)
154DEFINE_multistring = _wrapper(gflags.DEFINE_multistring)
155DEFINE_multi_int = _wrapper(gflags.DEFINE_multi_int)
156
157
158def DECLARE(name, module_string, flag_values=FLAGS):
159 if module_string not in sys.modules:
160 __import__(module_string, globals(), locals())
161 if name not in flag_values:
162 raise gflags.UnrecognizedFlag(
163 "%s not defined by %s" % (name, module_string))
164
165
166# __GLOBAL FLAGS ONLY__
167# Define any app-specific flags in their own files, docs at:
168# http://code.google.com/p/python-gflags/source/browse/trunk/gflags.py#39
169
170# TODO(sirp): move this out to an application specific setting when we create
171# Nova/Glance common library
172DEFINE_string('sql_connection',
173 'sqlite:///%s/glance.sqlite' % os.path.abspath("./"),
174 'connection string for sql database')
175DEFINE_bool('verbose', False, 'show debug output')
0176
=== added file 'glance/common/server.py'
--- glance/common/server.py 1970-01-01 00:00:00 +0000
+++ glance/common/server.py 2010-09-29 20:56:47 +0000
@@ -0,0 +1,144 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 United States Government as represented by the
4# Administrator of the National Aeronautics and Space Administration.
5# All Rights Reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License. You may obtain
9# a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16# License for the specific language governing permissions and limitations
17# under the License.
18
19"""
20Base functionality for nova daemons - gradually being replaced with twistd.py.
21"""
22
23import daemon
24from daemon import pidlockfile
25import logging
26import logging.handlers
27import os
28import signal
29import sys
30import time
31
32from glance.common import flags
33
34
35FLAGS = flags.FLAGS
36flags.DEFINE_bool('daemonize', False, 'daemonize this process')
37# NOTE(termie): right now I am defaulting to using syslog when we daemonize
38# it may be better to do something else -shrug-
39# NOTE(Devin): I think we should let each process have its own log file
40# and put it in /var/logs/nova/(appname).log
41# This makes debugging much easier and cuts down on sys log
42# clutter.
43flags.DEFINE_bool('use_syslog', True, 'output to syslog when daemonizing')
44flags.DEFINE_string('logfile', None, 'log file to output to')
45flags.DEFINE_string('pidfile', None, 'pid file to output to')
46flags.DEFINE_string('working_directory', './', 'working directory...')
47flags.DEFINE_integer('uid', os.getuid(), 'uid under which to run')
48flags.DEFINE_integer('gid', os.getgid(), 'gid under which to run')
49
50
51def stop(pidfile):
52 """
53 Stop the daemon
54 """
55 # Get the pid from the pidfile
56 try:
57 pid = int(open(pidfile,'r').read().strip())
58 except IOError:
59 message = "pidfile %s does not exist. Daemon not running?\n"
60 sys.stderr.write(message % pidfile)
61 return # not an error in a restart
62
63 # Try killing the daemon process
64 try:
65 while 1:
66 os.kill(pid, signal.SIGTERM)
67 time.sleep(0.1)
68 except OSError, err:
69 err = str(err)
70 if err.find("No such process") > 0:
71 if os.path.exists(pidfile):
72 os.remove(pidfile)
73 else:
74 print str(err)
75 sys.exit(1)
76
77
78def serve(name, main):
79 """Controller for server"""
80 argv = FLAGS(sys.argv)
81
82 if not FLAGS.pidfile:
83 FLAGS.pidfile = '%s.pid' % name
84
85 logging.debug("Full set of FLAGS: \n\n\n")
86 for flag in FLAGS:
87 logging.debug("%s : %s", flag, FLAGS.get(flag, None))
88
89 action = 'start'
90 if len(argv) > 1:
91 action = argv.pop()
92
93 if action == 'stop':
94 stop(FLAGS.pidfile)
95 sys.exit()
96 elif action == 'restart':
97 stop(FLAGS.pidfile)
98 elif action == 'start':
99 pass
100 else:
101 print 'usage: %s [options] [start|stop|restart]' % argv[0]
102 sys.exit(1)
103 daemonize(argv, name, main)
104
105
106def daemonize(args, name, main):
107 """Does the work of daemonizing the process"""
108 logging.getLogger('amqplib').setLevel(logging.WARN)
109 if FLAGS.daemonize:
110 logger = logging.getLogger()
111 formatter = logging.Formatter(
112 name + '(%(name)s): %(levelname)s %(message)s')
113 if FLAGS.use_syslog and not FLAGS.logfile:
114 syslog = logging.handlers.SysLogHandler(address='/dev/log')
115 syslog.setFormatter(formatter)
116 logger.addHandler(syslog)
117 else:
118 if not FLAGS.logfile:
119 FLAGS.logfile = '%s.log' % name
120 logfile = logging.FileHandler(FLAGS.logfile)
121 logfile.setFormatter(formatter)
122 logger.addHandler(logfile)
123 stdin, stdout, stderr = None, None, None
124 else:
125 stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr
126
127 if FLAGS.verbose:
128 logging.getLogger().setLevel(logging.DEBUG)
129 else:
130 logging.getLogger().setLevel(logging.WARNING)
131
132 with daemon.DaemonContext(
133 detach_process=FLAGS.daemonize,
134 working_directory=FLAGS.working_directory,
135 pidfile=pidlockfile.TimeoutPIDLockFile(FLAGS.pidfile,
136 acquire_timeout=1,
137 threaded=False),
138 stdin=stdin,
139 stdout=stdout,
140 stderr=stderr,
141 uid=FLAGS.uid,
142 gid=FLAGS.gid
143 ):
144 main(args)
0145
=== added file 'glance/common/utils.py'
--- glance/common/utils.py 1970-01-01 00:00:00 +0000
+++ glance/common/utils.py 2010-09-29 20:56:47 +0000
@@ -0,0 +1,204 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 United States Government as represented by the
4# Administrator of the National Aeronautics and Space Administration.
5# All Rights Reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License. You may obtain
9# a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16# License for the specific language governing permissions and limitations
17# under the License.
18
19"""
20System-level utilities and helper functions.
21"""
22
23import datetime
24import inspect
25import logging
26import os
27import random
28import subprocess
29import socket
30import sys
31
32from twisted.internet.threads import deferToThread
33
34from glance.common import exception
35from glance.common import flags
36from glance.common.exception import ProcessExecutionError
37
38
39FLAGS = flags.FLAGS
40TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
41
42def import_class(import_str):
43 """Returns a class from a string including module and class"""
44 mod_str, _sep, class_str = import_str.rpartition('.')
45 try:
46 __import__(mod_str)
47 return getattr(sys.modules[mod_str], class_str)
48 except (ImportError, ValueError, AttributeError):
49 raise exception.NotFound('Class %s cannot be found' % class_str)
50
51def import_object(import_str):
52 """Returns an object including a module or module and class"""
53 try:
54 __import__(import_str)
55 return sys.modules[import_str]
56 except ImportError:
57 cls = import_class(import_str)
58 return cls()
59
60def fetchfile(url, target):
61 logging.debug("Fetching %s" % url)
62# c = pycurl.Curl()
63# fp = open(target, "wb")
64# c.setopt(c.URL, url)
65# c.setopt(c.WRITEDATA, fp)
66# c.perform()
67# c.close()
68# fp.close()
69 execute("curl --fail %s -o %s" % (url, target))
70
71def execute(cmd, process_input=None, addl_env=None, check_exit_code=True):
72 logging.debug("Running cmd: %s", cmd)
73 env = os.environ.copy()
74 if addl_env:
75 env.update(addl_env)
76 obj = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
77 stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
78 result = None
79 if process_input != None:
80 result = obj.communicate(process_input)
81 else:
82 result = obj.communicate()
83 obj.stdin.close()
84 if obj.returncode:
85 logging.debug("Result was %s" % (obj.returncode))
86 if check_exit_code and obj.returncode <> 0:
87 (stdout, stderr) = result
88 raise ProcessExecutionError(exit_code=obj.returncode,
89 stdout=stdout,
90 stderr=stderr,
91 cmd=cmd)
92 return result
93
94
95def abspath(s):
96 return os.path.join(os.path.dirname(__file__), s)
97
98
99# TODO(sirp): when/if utils is extracted to common library, we should remove
100# the argument's default.
101#def default_flagfile(filename='nova.conf'):
102def default_flagfile(filename='glance.conf'):
103 for arg in sys.argv:
104 if arg.find('flagfile') != -1:
105 break
106 else:
107 if not os.path.isabs(filename):
108 # turn relative filename into an absolute path
109 script_dir = os.path.dirname(inspect.stack()[-1][1])
110 filename = os.path.abspath(os.path.join(script_dir, filename))
111 if os.path.exists(filename):
112 sys.argv = sys.argv[:1] + ['--flagfile=%s' % filename] + sys.argv[1:]
113
114
115def debug(arg):
116 logging.debug('debug in callback: %s', arg)
117 return arg
118
119
120def runthis(prompt, cmd, check_exit_code = True):
121 logging.debug("Running %s" % (cmd))
122 exit_code = subprocess.call(cmd.split(" "))
123 logging.debug(prompt % (exit_code))
124 if check_exit_code and exit_code <> 0:
125 raise ProcessExecutionError(exit_code=exit_code,
126 stdout=None,
127 stderr=None,
128 cmd=cmd)
129
130
131def generate_uid(topic, size=8):
132 return '%s-%s' % (topic, ''.join([random.choice('01234567890abcdefghijklmnopqrstuvwxyz') for x in xrange(size)]))
133
134
135def generate_mac():
136 mac = [0x02, 0x16, 0x3e, random.randint(0x00, 0x7f),
137 random.randint(0x00, 0xff), random.randint(0x00, 0xff)
138 ]
139 return ':'.join(map(lambda x: "%02x" % x, mac))
140
141
142def last_octet(address):
143 return int(address.split(".")[-1])
144
145
146def get_my_ip():
147 """Returns the actual ip of the local machine."""
148 if getattr(FLAGS, 'fake_tests', None):
149 return '127.0.0.1'
150 try:
151 csock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
152 csock.connect(('www.google.com', 80))
153 (addr, port) = csock.getsockname()
154 csock.close()
155 return addr
156 except socket.gaierror as ex:
157 logging.warn("Couldn't get IP, using 127.0.0.1 %s", ex)
158 return "127.0.0.1"
159
160
161def isotime(at=None):
162 if not at:
163 at = datetime.datetime.utcnow()
164 return at.strftime(TIME_FORMAT)
165
166
167def parse_isotime(timestr):
168 return datetime.datetime.strptime(timestr, TIME_FORMAT)
169
170
171class LazyPluggable(object):
172 """A pluggable backend loaded lazily based on some value."""
173
174 def __init__(self, pivot, **backends):
175 self.__backends = backends
176 self.__pivot = pivot
177 self.__backend = None
178
179 def __get_backend(self):
180 if not self.__backend:
181 backend_name = self.__pivot.value
182 if backend_name not in self.__backends:
183 raise exception.Error('Invalid backend: %s' % backend_name)
184
185 backend = self.__backends[backend_name]
186 if type(backend) == type(tuple()):
187 name = backend[0]
188 fromlist = backend[1]
189 else:
190 name = backend
191 fromlist = backend
192
193 self.__backend = __import__(name, None, None, fromlist)
194 logging.info('backend %s', self.__backend)
195 return self.__backend
196
197 def __getattr__(self, key):
198 backend = self.__get_backend()
199 return getattr(backend, key)
200
201def deferredToThread(f):
202 def g(*args, **kwargs):
203 return deferToThread(f, *args, **kwargs)
204 return g
0205
=== added file 'glance/common/wsgi.py'
--- glance/common/wsgi.py 1970-01-01 00:00:00 +0000
+++ glance/common/wsgi.py 2010-09-29 20:56:47 +0000
@@ -0,0 +1,298 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 United States Government as represented by the
4# Administrator of the National Aeronautics and Space Administration.
5# Copyright 2010 OpenStack LLC.
6# All Rights Reserved.
7#
8# Licensed under the Apache License, Version 2.0 (the "License"); you may
9# not use this file except in compliance with the License. You may obtain
10# a copy of the License at
11#
12# http://www.apache.org/licenses/LICENSE-2.0
13#
14# Unless required by applicable law or agreed to in writing, software
15# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17# License for the specific language governing permissions and limitations
18# under the License.
19
20"""
21Utility methods for working with WSGI servers
22"""
23
24import logging
25import sys
26
27import eventlet
28import eventlet.wsgi
29eventlet.patcher.monkey_patch(all=False, socket=True)
30import routes
31import routes.middleware
32import webob.dec
33import webob.exc
34
35
36logging.getLogger("routes.middleware").addHandler(logging.StreamHandler())
37
38
39def run_server(application, port):
40 """Run a WSGI server with the given application."""
41 sock = eventlet.listen(('0.0.0.0', port))
42 eventlet.wsgi.server(sock, application)
43
44
45class Application(object):
46# TODO(gundlach): I think we should toss this class, now that it has no
47# purpose.
48 """Base WSGI application wrapper. Subclasses need to implement __call__."""
49
50 def __call__(self, environ, start_response):
51 r"""Subclasses will probably want to implement __call__ like this:
52
53 @webob.dec.wsgify
54 def __call__(self, req):
55 # Any of the following objects work as responses:
56
57 # Option 1: simple string
58 res = 'message\n'
59
60 # Option 2: a nicely formatted HTTP exception page
61 res = exc.HTTPForbidden(detail='Nice try')
62
63 # Option 3: a webob Response object (in case you need to play with
64 # headers, or you want to be treated like an iterable, or or or)
65 res = Response();
66 res.app_iter = open('somefile')
67
68 # Option 4: any wsgi app to be run next
69 res = self.application
70
71 # Option 5: you can get a Response object for a wsgi app, too, to
72 # play with headers etc
73 res = req.get_response(self.application)
74
75 # You can then just return your response...
76 return res
77 # ... or set req.response and return None.
78 req.response = res
79
80 See the end of http://pythonpaste.org/webob/modules/dec.html
81 for more info.
82 """
83 raise NotImplementedError("You must implement __call__")
84
85
86class Middleware(Application):
87 """
88 Base WSGI middleware wrapper. These classes require an application to be
89 initialized that will be called next. By default the middleware will
90 simply call its wrapped app, or you can override __call__ to customize its
91 behavior.
92 """
93
94 def __init__(self, application): # pylint: disable-msg=W0231
95 self.application = application
96
97 @webob.dec.wsgify
98 def __call__(self, req): # pylint: disable-msg=W0221
99 """Override to implement middleware behavior."""
100 return self.application
101
102
103class Debug(Middleware):
104 """Helper class that can be inserted into any WSGI application chain
105 to get information about the request and response."""
106
107 @webob.dec.wsgify
108 def __call__(self, req):
109 print ("*" * 40) + " REQUEST ENVIRON"
110 for key, value in req.environ.items():
111 print key, "=", value
112 print
113 resp = req.get_response(self.application)
114
115 print ("*" * 40) + " RESPONSE HEADERS"
116 for (key, value) in resp.headers.iteritems():
117 print key, "=", value
118 print
119
120 resp.app_iter = self.print_generator(resp.app_iter)
121
122 return resp
123
124 @staticmethod
125 def print_generator(app_iter):
126 """
127 Iterator that prints the contents of a wrapper string iterator
128 when iterated.
129 """
130 print ("*" * 40) + " BODY"
131 for part in app_iter:
132 sys.stdout.write(part)
133 sys.stdout.flush()
134 yield part
135 print
136
137
138class Router(object):
139 """
140 WSGI middleware that maps incoming requests to WSGI apps.
141 """
142
143 def __init__(self, mapper):
144 """
145 Create a router for the given routes.Mapper.
146
147 Each route in `mapper` must specify a 'controller', which is a
148 WSGI app to call. You'll probably want to specify an 'action' as
149 well and have your controller be a wsgi.Controller, who will route
150 the request to the action method.
151
152 Examples:
153 mapper = routes.Mapper()
154 sc = ServerController()
155
156 # Explicit mapping of one route to a controller+action
157 mapper.connect(None, "/svrlist", controller=sc, action="list")
158
159 # Actions are all implicitly defined
160 mapper.resource("server", "servers", controller=sc)
161
162 # Pointing to an arbitrary WSGI app. You can specify the
163 # {path_info:.*} parameter so the target app can be handed just that
164 # section of the URL.
165 mapper.connect(None, "/v1.0/{path_info:.*}", controller=BlogApp())
166 """
167 self.map = mapper
168 self._router = routes.middleware.RoutesMiddleware(self._dispatch,
169 self.map)
170
171 @webob.dec.wsgify
172 def __call__(self, req):
173 """
174 Route the incoming request to a controller based on self.map.
175 If no match, return a 404.
176 """
177 return self._router
178
179 @staticmethod
180 @webob.dec.wsgify
181 def _dispatch(req):
182 """
183 Called by self._router after matching the incoming request to a route
184 and putting the information into req.environ. Either returns 404
185 or the routed WSGI app's response.
186 """
187 match = req.environ['wsgiorg.routing_args'][1]
188 if not match:
189 return webob.exc.HTTPNotFound()
190 app = match['controller']
191 return app
192
193
194class Controller(object):
195 """
196 WSGI app that reads routing information supplied by RoutesMiddleware
197 and calls the requested action method upon itself. All action methods
198 must, in addition to their normal parameters, accept a 'req' argument
199 which is the incoming webob.Request. They raise a webob.exc exception,
200 or return a dict which will be serialized by requested content type.
201 """
202
203 @webob.dec.wsgify
204 def __call__(self, req):
205 """
206 Call the method specified in req.environ by RoutesMiddleware.
207 """
208 arg_dict = req.environ['wsgiorg.routing_args'][1]
209 action = arg_dict['action']
210 method = getattr(self, action)
211 del arg_dict['controller']
212 del arg_dict['action']
213 arg_dict['req'] = req
214 result = method(**arg_dict)
215 if type(result) is dict:
216 return self._serialize(result, req)
217 else:
218 return result
219
220 def _serialize(self, data, request):
221 """
222 Serialize the given dict to the response type requested in request.
223 Uses self._serialization_metadata if it exists, which is a dict mapping
224 MIME types to information needed to serialize to that type.
225 """
226 # FIXME(sirp): type(self) should just be `self`
227 _metadata = getattr(type(self), "_serialization_metadata", {})
228 serializer = Serializer(request.environ, _metadata)
229 return serializer.to_content_type(data)
230
231
232class Serializer(object):
233 """
234 Serializes a dictionary to a Content Type specified by a WSGI environment.
235 """
236
237 def __init__(self, environ, metadata=None):
238 """
239 Create a serializer based on the given WSGI environment.
240 'metadata' is an optional dict mapping MIME types to information
241 needed to serialize a dictionary to that type.
242 """
243 self.environ = environ
244 self.metadata = metadata or {}
245 self._methods = {
246 'application/json': self._to_json,
247 'application/xml': self._to_xml}
248
249 def to_content_type(self, data):
250 """
251 Serialize a dictionary into a string. The format of the string
252 will be decided based on the Content Type requested in self.environ:
253 by Accept: header, or by URL suffix.
254 """
255 # FIXME(sirp): for now, supporting json only
256 #mimetype = 'application/xml'
257 mimetype = 'application/json'
258 # TODO(gundlach): determine mimetype from request
259 return self._methods.get(mimetype, repr)(data)
260
261 def _to_json(self, data):
262 import json
263 return json.dumps(data)
264
265 def _to_xml(self, data):
266 metadata = self.metadata.get('application/xml', {})
267 # We expect data to contain a single key which is the XML root.
268 root_key = data.keys()[0]
269 from xml.dom import minidom
270 doc = minidom.Document()
271 node = self._to_xml_node(doc, metadata, root_key, data[root_key])
272 return node.toprettyxml(indent=' ')
273
274 def _to_xml_node(self, doc, metadata, nodename, data):
275 """Recursive method to convert data members to XML nodes."""
276 result = doc.createElement(nodename)
277 if type(data) is list:
278 singular = metadata.get('plurals', {}).get(nodename, None)
279 if singular is None:
280 if nodename.endswith('s'):
281 singular = nodename[:-1]
282 else:
283 singular = 'item'
284 for item in data:
285 node = self._to_xml_node(doc, metadata, singular, item)
286 result.appendChild(node)
287 elif type(data) is dict:
288 attrs = metadata.get('attributes', {}).get(nodename, {})
289 for k, v in data.items():
290 if k in attrs:
291 result.setAttribute(k, str(v))
292 else:
293 node = self._to_xml_node(doc, metadata, k, v)
294 result.appendChild(node)
295 else: # atom
296 node = doc.createTextNode(str(data))
297 result.appendChild(node)
298 return result
0299
=== modified file 'glance/parallax/__init__.py'
--- glance/parallax/__init__.py 2010-08-24 04:32:57 +0000
+++ glance/parallax/__init__.py 2010-09-29 20:56:47 +0000
@@ -1,16 +1,21 @@
1class ImageRegistry(object):1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2 """2
3 {3# Copyright 2010 United States Government as represented by the
4 "uuid": uuid,4# Administrator of the National Aeronautics and Space Administration.
5 "slug": slug,5# All Rights Reserved.
6 "name": name,6#
7 "objects": [7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8 { "uri": obj1_uri },8# not use this file except in compliance with the License. You may obtain
9 { "uri": obj2_uri }9# a copy of the License at
10 ]10#
11 }11# http://www.apache.org/licenses/LICENSE-2.0
12 """12#
13 def __init__(self):13# Unless required by applicable law or agreed to in writing, software
14 pass14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1515# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1616# License for the specific language governing permissions and limitations
17# under the License.
18
19"""
20Parallax API
21"""
1722
=== added directory 'glance/parallax/api'
=== added file 'glance/parallax/api/__init__.py'
--- glance/parallax/api/__init__.py 1970-01-01 00:00:00 +0000
+++ glance/parallax/api/__init__.py 2010-09-29 20:56:47 +0000
@@ -0,0 +1,49 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 United States Government as represented by the
4# Administrator of the National Aeronautics and Space Administration.
5# All Rights Reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License. You may obtain
9# a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16# License for the specific language governing permissions and limitations
17# under the License.
18
19"""
20Parallax API controllers.
21"""
22
23import json
24import time
25
26import routes
27import webob.dec
28import webob.exc
29import webob
30
31from glance.common import flags
32from glance.common import utils
33from glance.common import wsgi
34from glance.parallax.api import images
35
36
37FLAGS = flags.FLAGS
38
39
40class API(wsgi.Router):
41 """WSGI entry point for all Parallax requests."""
42
43 def __init__(self):
44 # TODO(sirp): should we add back the middleware for parallax
45 mapper = routes.Mapper()
46 mapper.resource("image", "images", controller=images.Controller(),
47 collection={'detail': 'GET'})
48 super(API, self).__init__(mapper)
49
050
=== added file 'glance/parallax/api/images.py'
--- glance/parallax/api/images.py 1970-01-01 00:00:00 +0000
+++ glance/parallax/api/images.py 2010-09-29 20:56:47 +0000
@@ -0,0 +1,82 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 OpenStack LLC.
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18"""
19Parllax Image controller
20"""
21
22
23from glance.common import wsgi
24from glance.common import db
25from glance.common import exception
26from webob import exc
27
28
29class Controller(wsgi.Controller):
30 """Image Controller """
31
32 # TODO(sirp): this is not currently used, but should eventually
33 # incorporate it
34 _serialization_metadata = {
35 'application/xml': {
36 "attributes": {
37 "image": [ "id", "name", "updated", "created", "status",
38 "serverId", "progress" ]
39 }
40 }
41 }
42
43 def index(self, req):
44 """Index is not currently supported """
45 raise exc.HTTPNotImplemented()
46
47 def detail(self, req):
48 """Detail is not currently supported """
49 raise exc.HTTPNotImplemented()
50
51 def show(self, req, id):
52 """Return data about the given image id."""
53 try:
54 image = db.image_get(None, id)
55 except exception.NotFound:
56 raise exc.HTTPNotFound()
57
58 file_dicts = [dict(location=f.location, size=f.size)
59 for f in image.files]
60
61 metadata_dicts = [dict(key=m.key, value=m.value)
62 for m in image.metadata]
63
64 return dict(id=image.id,
65 name=image.name,
66 state=image.state,
67 public=image.public,
68 files=file_dicts,
69 metadata=metadata_dicts)
70
71 def delete(self, req, id):
72 """Delete is not currently supported """
73 raise exc.HTTPNotImplemented()
74
75 def create(self, req):
76 """Create is not currently supported """
77 raise exc.HTTPNotImplemented()
78
79 def update(self, req, id):
80 """Update is not currently supported """
81 raise exc.HTTPNotImplemented()
82
083
=== added file 'tests/test_data.py'
--- tests/test_data.py 1970-01-01 00:00:00 +0000
+++ tests/test_data.py 2010-09-29 20:56:47 +0000
@@ -0,0 +1,49 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 OpenStack, LLC
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18from glance.common.db import api
19
20
21def make_fake_image():
22 """Create a fake image record """
23 image = api.image_create(
24 None,
25 dict(name="Test Image",
26 state="available",
27 public=True,
28 image_type="tarball"))
29
30 api.image_file_create(
31 None,
32 dict(image_id=image.id,
33 location="swift://myacct/mycontainer/obj.tar.gz.0",
34 size=101))
35 api.image_file_create(
36 None,
37 dict(image_id=image.id,
38 location="swift://myacct/mycontainer/obj.tar.gz.1",
39 size=101))
40
41 api.image_metadatum_create(
42 None,
43 dict(image_id=image.id,
44 key="testkey",
45 value="testvalue"))
46
47
48if __name__ == "__main__":
49 make_fake_image()

Subscribers

People subscribed via source and target branches