Merge lp:~raxnetworking/nova/melange into lp:~hudson-openstack/nova/trunk

Proposed by Trey Morris
Status: Superseded
Proposed branch: lp:~raxnetworking/nova/melange
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 10983 lines (+10459/-10)
87 files modified
.bzrignore (+6/-9)
.mailmap (+1/-0)
Authors (+4/-0)
bin/melange (+66/-0)
bin/melange-client (+183/-0)
bin/melange-delete-deallocated-ips (+57/-0)
bin/melange-manage (+125/-0)
bin/nova-manage (+1/-1)
etc/melange/melange.conf.sample (+77/-0)
etc/melange/melange.conf.test (+61/-0)
melange/README (+14/-0)
melange/__init__.py (+29/-0)
melange/common/__init__.py (+16/-0)
melange/common/auth.py (+97/-0)
melange/common/client.py (+64/-0)
melange/common/config.py (+36/-0)
melange/common/exception.py (+34/-0)
melange/common/extensions.py (+437/-0)
melange/common/pagination.py (+108/-0)
melange/common/utils.py (+171/-0)
melange/common/wsgi.py (+383/-0)
melange/db/__init__.py (+43/-0)
melange/db/sqlalchemy/__init__.py (+16/-0)
melange/db/sqlalchemy/api.py (+213/-0)
melange/db/sqlalchemy/mappers.py (+53/-0)
melange/db/sqlalchemy/migrate_repo/README (+4/-0)
melange/db/sqlalchemy/migrate_repo/__init__.py (+18/-0)
melange/db/sqlalchemy/migrate_repo/manage.py (+21/-0)
melange/db/sqlalchemy/migrate_repo/migrate.cfg (+20/-0)
melange/db/sqlalchemy/migrate_repo/schema.py (+65/-0)
melange/db/sqlalchemy/migrate_repo/versions/001_add_ip_blocks_table.py (+48/-0)
melange/db/sqlalchemy/migrate_repo/versions/002_add_ip_addresses_table.py (+54/-0)
melange/db/sqlalchemy/migrate_repo/versions/003_add_type_to_ip_blocks.py (+33/-0)
melange/db/sqlalchemy/migrate_repo/versions/004_add_ip_nat_table.py (+56/-0)
melange/db/sqlalchemy/migrate_repo/versions/005_add_soft_delete_to_blocks_addresses_and_ip_nats.py (+37/-0)
melange/db/sqlalchemy/migrate_repo/versions/006_add_deallocated_to_ip_addresses.py (+35/-0)
melange/db/sqlalchemy/migrate_repo/versions/007_add_policy_table.py (+51/-0)
melange/db/sqlalchemy/migrate_repo/versions/008_add_ip_ranges_table.py (+55/-0)
melange/db/sqlalchemy/migrate_repo/versions/009_add_policy_id_to_ip_blocks.py (+38/-0)
melange/db/sqlalchemy/migrate_repo/versions/010_add_ip_octets_table.py (+54/-0)
melange/db/sqlalchemy/migrate_repo/versions/011_add_tenant_id_to_ip_blocks.py (+33/-0)
melange/db/sqlalchemy/migrate_repo/versions/012_add_tenant_id_to_policies.py (+33/-0)
melange/db/sqlalchemy/migrate_repo/versions/013_add_parent_id_to_ip_blocks.py (+36/-0)
melange/db/sqlalchemy/migrate_repo/versions/014_add_is_full_to_ip_blocks.py (+35/-0)
melange/db/sqlalchemy/migrate_repo/versions/015_gateway_and_broadcast_addresses_to_ip_block.py (+38/-0)
melange/db/sqlalchemy/migrate_repo/versions/016_add_deallocated_at_to_ip_addresses.py (+34/-0)
melange/db/sqlalchemy/migrate_repo/versions/017_remove_broadcast_address_and_rename_gateway_address.py (+37/-0)
melange/db/sqlalchemy/migrate_repo/versions/018_add_dns_fields_to_ip_blocks.py (+36/-0)
melange/db/sqlalchemy/migrate_repo/versions/__init__.py (+18/-0)
melange/db/sqlalchemy/migration.py (+124/-0)
melange/db/sqlalchemy/session.py (+91/-0)
melange/extensions/__init__.py (+16/-0)
melange/ipam/__init__.py (+16/-0)
melange/ipam/client.py (+178/-0)
melange/ipam/models.py (+820/-0)
melange/ipam/service.py (+426/-0)
melange/ipv6/__init__.py (+16/-0)
melange/ipv6/rfc2462_generator.py (+42/-0)
melange/ipv6/tenant_based_generator.py (+47/-0)
melange/tests/__init__.py (+74/-0)
melange/tests/factories/__init__.py (+16/-0)
melange/tests/factories/models.py (+67/-0)
melange/tests/functional/__init__.py (+119/-0)
melange/tests/functional/server.py (+76/-0)
melange/tests/functional/test_cli.py (+422/-0)
melange/tests/functional/test_service.py (+62/-0)
melange/tests/unit/__init__.py (+91/-0)
melange/tests/unit/extensions/__init__.py (+15/-0)
melange/tests/unit/extensions/foxinsocks.py (+96/-0)
melange/tests/unit/mock_generator.py (+28/-0)
melange/tests/unit/test_auth.py (+225/-0)
melange/tests/unit/test_config.py (+49/-0)
melange/tests/unit/test_extensions.py (+227/-0)
melange/tests/unit/test_ipam_models.py (+1421/-0)
melange/tests/unit/test_ipam_service.py (+1612/-0)
melange/tests/unit/test_pagination.py (+73/-0)
melange/tests/unit/test_rfc2462_ipv6_generator.py (+52/-0)
melange/tests/unit/test_sqlalchemy_api.py (+45/-0)
melange/tests/unit/test_tenant_based_ipv6_generator.py (+52/-0)
melange/tests/unit/test_utils.py (+194/-0)
melange/tests/unit/test_versions.py (+34/-0)
melange/tests/unit/test_wsgi.py (+327/-0)
melange/version.py (+47/-0)
melange/versions.py (+67/-0)
run_tests.sh (+1/-0)
tools/install_venv.py (+4/-0)
tools/pip-requires (+3/-0)
To merge this branch: bzr merge lp:~raxnetworking/nova/melange
Reviewer Review Type Date Requested Status
Rick Harris (community) Needs Fixing
Jay Pipes (community) Needs Fixing
Sandy Walsh (community) Needs Information
Vish Ishaya (community) Needs Information
Review via email: mp+71917@code.launchpad.net

This proposal has been superseded by a proposal from 2011-08-25.

Description of the change

adds melange to nova

melange is currently an IP or L3 management service, but will probably expand into more of a "network identifier" management service which may include L2 data in the future.

To post a comment you must log in.
lp:~raxnetworking/nova/melange updated
1339. By Trey Morris

moved double author to .mailmap

Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

Getting some errors http://paste.openstack.org/show/2187/
(are you running novaclient 2.6.1?)

review: Needs Information
Revision history for this message
Josh Kearney (jk0) wrote :

> Getting some errors http://paste.openstack.org/show/2187/
> (are you running novaclient 2.6.1?)

That's odd. All tests pass for me running the latest novaclient.

Revision history for this message
Vish Ishaya (vishvananda) wrote :

This is looking good guys. First impression is that the code is pretty clean and well laid out. Quick nits in my first glance at the code:

Not sure about the changes to bzrignore. Why remove the ignores for logs and sqlite dbs?

3320 +required_dbs=['mysql','postgres','sqllite']
extra l in sqlite.

Are there some tests as well? I see reference to melage/tests/unit in a couple places, but no actual code

review: Needs Information
Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

ok, got the tests running again. Was running an older eventlet and the easy_install and pip were conflicting with two versions.

But melange tests are failing

http://paste.openstack.org/show/2188/

review: Needs Fixing
lp:~raxnetworking/nova/melange updated
1340. By Trey Morris

alphabetized melange imports

1341. By Trey Morris

correct copyright

Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

unit tests pass, but functional tests won't run
http://paste.openstack.org/show/2189/

review: Needs Fixing
Revision history for this message
Trey Morris (tr3buchet) wrote :

Vish, there are tests, this diff is truncated, yay launchpad. You'll have to use commandline diffs.

The tests are located in melange/tests/.

sqllite -> sqlite

As for the bzrignore changes:
*.DS_Store, .project, and .pydevproject
were simply moved, no harm no foul i guess

run_tests.err.log
run_tests.log
are replaced with *.log

nova.sqlite
clean.sqlite
tests.sqlite
are replaced with *.sqlite

lp:~raxnetworking/nova/melange updated
1342. By Trey Morris

fixed extra l in sqlite

Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

There is *so* much copy-paste from nova in here I feel dirty pressing Approve.

Why not just make it a proper Nova service under the network service and do it right?

review: Needs Information
Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

For unit tests we shouldn't have to create a db or copy/move a .conf file. Maybe for functional tests.

Revision history for this message
Josh Kearney (jk0) wrote :

> For unit tests we shouldn't have to create a db or copy/move a .conf file.
> Maybe for functional tests.

Hm, I never had to do that. Does it say that somewhere?

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

Some notes...

1) I probably missed a discussion somewhere, but weren't we planning on having melange as a completely separate service?

2) I don't think it's a good idea to have tools like bin/melange accessing or configuring the db API manually... this should be done through a client class only, and bin/melange shouldn't be configuring any database at all.

3) The bin/melange-manage has copyright headers having to do with Django's interactive shell, but I see no evidence of django interactive shell stuff being used there.

4) Agree with Vishy on some of the bzrignore stuff... for instance, what is this:

 34 +.ropeproject

?

5) Executables like bin/melange-delete-deallocated-ips should a) be combined into a single melange-manage tool, and b) Should not directly access db models or API... these should be client commands. These tools look like they were written assuming someone would always be logged into the box running the melange server; this should not be the case. An admin should be able to run bin/melange-manage and similar tools from anywhere, and a Melange client class should send HTTP commands to the Melange server, not directly interact with the underlying database...

6) Pretty much all of the code in bin/melange-manage should be in a melange.Client class, with the melange-manage executable being a simple CLI wrapper around the client calls, instead of being direct HTTP calls.

7)

1364 +def integer(value):
1365 + return int(value)

Really?

8) I would recommend removing all of the "extensions" code in /melange/extensions/. IMO, YAGNI, and it just makes things overly complicated.

9) Nobody is upgrading melange yet, so not sure any of the db-migrate scripts are necessary

Cheers,
jay

review: Needs Fixing
Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

+1 to Jay's feedback.

This should really be our motivation to push on openstack-common.

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

Where we are putting common code (that will eventually become openstack.common...)

https://github.com/openstack/openstack-skeleton/

Revision history for this message
Rick Harris (rconradharris) wrote :

+1 on Jay's comments

Some other minor nits:

> 268 +from gettext import gettext as _

Nova uses this pattern for importing gettext (might be a good idea to match):

    import gettext
    gettext.install('melange', unicode=1)

> 1005 + def do_request(self, method, path, body=None, headers={}, params={}):

Usually not a good idea to use mutable types as default arguments since the
default is shared across invocations which can lead to some very strange bugs.
A better approach is usually to do something like:

    def do_request(self, method, path, body=None, headers=None, params=None):
        headers = headers or {}
        params = params or {}

        # OR

        if not headers:
            headers = {}

> 1384 +def boolean(subject):

Nova already has a pretty well tested boolean to string method
`utils.bool_from_str`. It would probably be a good idea to use that if
possible.

> === added file 'melange/common/utils.py'
>

Pertains to all of melange/common/ really:

Since melange resides in the same source tree as Nova, it would probably be a
good idea to pull as much as this as possible from nova.utils directly (rather than duplicate it).

As mentioned above, an even better solution is to have openstack.common as an external dependency (at some point).

review: Needs Fixing
Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

+1 to Rick's suggestion of, at least, fixing the imports to use nova's existing code base.

lp:~raxnetworking/nova/melange updated
1343. By Santhosh Kumar Muniraj

Santhosh/Deepak | Fixed bug where tenant_id was not being passed for allocating Ipv6 addresses through network controller.

1344. By Rajaram Mallya

Rajaram/Vinkesh|merge from nova

1345. By Rajaram Mallya

Rajaram/Vinkesh|used common framework code that exists in the openstack-common project

1346. By Rajaram Mallya

Rajaram/Vinkesh|some more misc cleanup to use openstack-common project

1347. By Rajaram Mallya

Rajaram/Vinkesh | Renamed melange-manage to melange-client. Added db sync, upgrade and downgrade commands in melange-manage

1348. By Rajaram Mallya

Rajaram/Vinkesh | Fixed some pep8 errors

1349. By Rajaram Mallya

Vinkesh/Rajaram|fixed gettext to work like in nova as per Rick Harris' suggestion

1350. By Rajaram Mallya

Rajaram/Vinkesh | Removed usages of mutable default arguments

1351. By Rajaram Mallya

Rajaram/Vinkesh | moved common/data_types.py to Converter class in models.py to remove confusion caused by the Integer and Boolean methods

1352. By Rajaram Mallya

Rajaram/Vinkesh | extracted melange client code from CLI to ipam/client. Removed unnecessary bzrignore.

1353. By Rajaram Mallya

Rajaram/Vinkesh | Simplified models mapping in db api

1354. By Rajaram Mallya

Rajaram/Vinkesh | Renamed utils.Method to utils.MethodInspector. Removed unecessary bzrignore entries

1355. By Rajaram Mallya

Rajaram/Vinkesh | Switched to sqlite for tests. Cleaned up migration scripts.

1356. By Rajaram Mallya

Rajaram/Vinkesh|moved extensions folder inside melange

1357. By Rajaram Mallya

merge from nova

1358. By Rajaram Mallya

Rajaram/Vinkesh| updated readme file

1359. By Rajaram Mallya

Rajaram/Vinkesh| used wsgi.resource from openstack.common

1360. By Rajaram Mallya

merge from nova trunk

1361. By Rajaram Mallya

Rajaram| Newline between imports and copyright header, imports only import modules, doc strings as per HACKING guide

1362. By Rajaram Mallya

Rajaram|missed a couple of pep8 voilations

1363. By Rajaram Mallya

Vinkesh/Rajaram| Fixes as per Brain's comments:
1. :moved gettext.install to melange/__init__
2. melange-client raises errros only on option.verbose
3. Used urlparse.urljoin where applicable
4. Fixed version in bin/melange* scripts
and other misc fixes according to HACKING or style recomendations

1364. By Rajaram Mallya

Rajaram/Vinkesh|fixed more imports to import only modules and misc style improvements

1365. By Rajaram Mallya

Rajaram/Vinkesh | Changed openstack.common imports in melange common to module level labels

1366. By Rajaram Mallya

Rajaram/Vinkesh | Made MelangeError a subclass of openstack common's OpenstackException, removed usages of merge_dicts

1367. By Rajaram Mallya

Vinkesh\Rajaram|made keystone auth optional in melange-client

1368. By Rajaram Mallya

Vinkesh/Rajaram|changed wsgi in melange.common to use serializers from openstack.common

1369. By Vinkesh Banka

Vinkesh/Rajaram| Added tests for melange-manage db_sync and db_upgrade commands

1370. By Rajaram Mallya

Rajaram/Vinkesh | Removed overridden get_content_type in wsgi.Request class

1371. By Rajaram Mallya

Vinkesh/Rajaram|Removed extensions code and started using openstack common's extension code

1372. By Rajaram Mallya

Rajaram/Vinkesh|ipaddress now stores used_by_tenant to denote tenant using the ipaddress and used_by_device for the instance on which the ip_address is allocated

1373. By Rajaram Mallya

Vinkesh/Rajaram|Removed non tenanted resources from api

1374. By Rajaram Mallya

Vinkesh/Rajaram|Removed admin actions as after removing non tenant scoped resources, we dont have any admin actions

1375. By Rajaram Mallya

Merged from nova trunk

1376. By Rajaram Mallya

Rajaram/Vinkesh|nat resources in api are tenant scoped

1377. By Rajaram Mallya

Rajaram/Vinkesh | Non Tenanted resources can only be accessed by admins

1378. By Rajaram Mallya

Rajaram/Vinkesh| mandated PasteDeploy>=1.5 in pip_requires since melange uses call URI scheme from PasteDeploy 1.5

1379. By Rajaram Mallya

Rajaram/Vinkesh|added route to search for allocated ip addresses

1380. By Rajaram Mallya

Rajaram/Vinkesh | Added CLI commands for ip_address

1381. By Rajaram Mallya

Rajaram/Vinkesh | Removed some unused code. Added few tests. Miscellaneous small refactoring

1382. By Rajaram Mallya

Merged from nova trunk

1383. By Rajaram Mallya

Rajaram/Vinkesh | Cleaned up the code a bit. Small style fixes

1384. By Rajaram Mallya

Rajaram/Vinkesh| fixed bug in melange-delete-deallocated-ips where it wasnt loading the config file

1385. By Vinkesh Banka

Rajaram/Vinkesh | Consolidated migrations. Removed soft delete

1386. By Vinkesh Banka

Rajaram/Vinkesh | Added retries while allocating ips to fix concurrency problem

1387. By Vinkesh Banka

Rajaram/Vinkesh | AllocatedIps index now does not show deallocated IPs

1388. By Vinkesh Banka

Rajaram/Vinkesh | Added IpRoute model. Started using melange.conf.sample for tests

1389. By Vinkesh Banka

Rajaram/Vinkesh | Exposed CRUD APIs for ip_routes

1390. By Vinkesh Banka

Rajaram/Vinkesh | Added CLI for IpRoute

1391. By Vinkesh Banka

Rajaram/Vinkesh | Added ip_routes in ip_allocations' payload

1392. By Vinkesh Banka

Vinkesh | Validating gateway address is a valid address before IpBlock create/update

1393. By Vinkesh Banka

Vinkesh | Displaying an error message which asks users to provide a tenant_id in CLI if needed

1394. By Rajaram Mallya

Rajaram/Vinkesh|Changed ip allocation algo to not load all allocated ips at once

1395. By Rajaram Mallya

Vinkesh/Rajaram|moved ipv4 generation algo to a plugable module

Unmerged revisions

1395. By Rajaram Mallya

Vinkesh/Rajaram|moved ipv4 generation algo to a plugable module

1394. By Rajaram Mallya

Rajaram/Vinkesh|Changed ip allocation algo to not load all allocated ips at once

1393. By Vinkesh Banka

Vinkesh | Displaying an error message which asks users to provide a tenant_id in CLI if needed

1392. By Vinkesh Banka

Vinkesh | Validating gateway address is a valid address before IpBlock create/update

1391. By Vinkesh Banka

Rajaram/Vinkesh | Added ip_routes in ip_allocations' payload

1390. By Vinkesh Banka

Rajaram/Vinkesh | Added CLI for IpRoute

1389. By Vinkesh Banka

Rajaram/Vinkesh | Exposed CRUD APIs for ip_routes

1388. By Vinkesh Banka

Rajaram/Vinkesh | Added IpRoute model. Started using melange.conf.sample for tests

1387. By Vinkesh Banka

Rajaram/Vinkesh | AllocatedIps index now does not show deallocated IPs

1386. By Vinkesh Banka

Rajaram/Vinkesh | Added retries while allocating ips to fix concurrency problem

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2011-07-26 05:50:05 +0000
3+++ .bzrignore 2011-08-25 11:33:54 +0000
4@@ -1,19 +1,16 @@
5-run_tests.err.log
6+*.log
7+*.sqlite
8+*.DS_Store
9+.project
10+.pydevproject
11 .nova-venv
12+.coverage
13 ChangeLog
14 _trial_temp
15 keys
16 networks
17-nova.sqlite
18 CA
19 nova/vcsversion.py
20-*.DS_Store
21-.project
22-.pydevproject
23-clean.sqlite
24-run_tests.log
25-tests.sqlite
26 nova/tests/instance-*
27 tags
28-.coverage
29 covhtml
30
31=== modified file '.mailmap'
32--- .mailmap 2011-08-09 21:45:31 +0000
33+++ .mailmap 2011-08-25 11:33:54 +0000
34@@ -55,3 +55,4 @@
35 <reldan@oscloud.ru> <enugaev@griddynamics.com>
36 <kshileev@gmail.com> <kshileev@griddynamics.com>
37 <nsokolov@griddynamics.com> <nsokolov@griddynamics.net>
38+<santhosh.m@thoughtworks.com> <santhom@thoughtworks.com>
39
40=== modified file 'Authors'
41--- Authors 2011-08-23 18:16:04 +0000
42+++ Authors 2011-08-25 11:33:54 +0000
43@@ -26,6 +26,7 @@
44 Dave Walker <DaveWalker@ubuntu.com>
45 David Pravec <David.Pravec@danix.org>
46 Dean Troyer <dtroyer@gmail.com>
47+Deepak N <deepak.n@thoughtworks.com>
48 Devendra Modium <dmodium@isi.edu>
49 Devin Carlen <devin.carlen@gmail.com>
50 Donal Lafferty <donal.lafferty@citrix.com>
51@@ -85,6 +86,7 @@
52 Nikolay Sokolov <nsokolov@griddynamics.com>
53 Nirmal Ranganathan <nirmal.ranganathan@rackspace.com>
54 Paul Voccio <paul@openstack.org>
55+Rajaram Mallya <rajarammallya@gmail.com>
56 Renuka Apte <renuka.apte@citrix.com>
57 Ricardo Carrillo Cruz <emaildericky@gmail.com>
58 Rick Clark <rick@openstack.org>
59@@ -95,6 +97,7 @@
60 Ryu Ishimoto <ryu@midokura.jp>
61 Salvatore Orlando <salvatore.orlando@eu.citrix.com>
62 Sandy Walsh <sandy.walsh@rackspace.com>
63+Santhosh Kumar Muniraj <santhosh.m@thoughtworks.com>
64 Sateesh Chodapuneedi <sateesh.chodapuneedi@citrix.com>
65 Scott Moser <smoser@ubuntu.com>
66 Soren Hansen <soren.hansen@rackspace.com>
67@@ -106,6 +109,7 @@
68 Troy Toman <troy.toman@rackspace.com>
69 Tushar Patil <tushar.vitthal.patil@gmail.com>
70 Vasiliy Shlykov <vash@vasiliyshlykov.org>
71+Vinkesh Banka <vinkeshb@thoughtworks.com>
72 Vishvananda Ishaya <vishvananda@gmail.com>
73 Vivek Y S <vivek.ys@gmail.com>
74 Vladimir Popovski <vladimir@zadarastorage.com>
75
76=== added file 'bin/melange'
77--- bin/melange 1970-01-01 00:00:00 +0000
78+++ bin/melange 2011-08-25 11:33:54 +0000
79@@ -0,0 +1,66 @@
80+#!/usr/bin/env python
81+# vim: tabstop=4 shiftwidth=4 softtabstop=4
82+
83+# Copyright 2011 OpenStack LLC.
84+# All Rights Reserved.
85+#
86+# Licensed under the Apache License, Version 2.0 (the "License"); you may
87+# not use this file except in compliance with the License. You may obtain
88+# a copy of the License at
89+#
90+# http://www.apache.org/licenses/LICENSE-2.0
91+#
92+# Unless required by applicable law or agreed to in writing, software
93+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
94+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
95+# License for the specific language governing permissions and limitations
96+# under the License.
97+import gettext
98+import optparse
99+import os
100+import re
101+import sys
102+import time
103+
104+# If ../melange/__init__.py exists, add ../ to Python search path, so that
105+# it will override what happens to be installed in /usr/(local/)lib/python...
106+possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
107+ os.pardir,
108+ os.pardir))
109+if os.path.exists(os.path.join(possible_topdir, 'melange', '__init__.py')):
110+ sys.path.insert(0, possible_topdir)
111+
112+gettext.install('melange', unicode=1)
113+
114+from melange.common import config
115+from melange.common import wsgi
116+from melange.db import db_api
117+
118+
119+def create_options(parser):
120+ """
121+ Sets up the CLI and config-file options that may be
122+ parsed and program commands.
123+ :param parser: The option parser
124+ """
125+ parser.add_option('-p', '--port', dest="port", metavar="PORT",
126+ type=int, default=9898,
127+ help="Port the Melange API host listens on. "
128+ "Default: %default")
129+ config.add_common_options(parser)
130+ config.add_log_options(parser)
131+
132+
133+if __name__ == '__main__':
134+ oparser = optparse.OptionParser(version='%%prog VERSION')
135+ create_options(oparser)
136+ (options, args) = config.parse_options(oparser)
137+ try:
138+ conf, app = config.load_paste_app('melange', options, args)
139+ db_api.configure_db(conf)
140+ server = wsgi.Server()
141+ server.start(app, options.get('port', conf['bind_port']),
142+ conf['bind_host'])
143+ server.wait()
144+ except RuntimeError, e:
145+ sys.exit("ERROR: %s" % e)
146
147=== added file 'bin/melange-client'
148--- bin/melange-client 1970-01-01 00:00:00 +0000
149+++ bin/melange-client 2011-08-25 11:33:54 +0000
150@@ -0,0 +1,183 @@
151+#!/usr/bin/env python
152+# vim: tabstop=4 shiftwidth=4 softtabstop=4
153+
154+# Copyright 2011 OpenStack LLC.
155+# All Rights Reserved.
156+#
157+# Licensed under the Apache License, Version 2.0 (the "License"); you may
158+# not use this file except in compliance with the License. You may obtain
159+# a copy of the License at
160+#
161+# http://www.apache.org/licenses/LICENSE-2.0
162+#
163+# Unless required by applicable law or agreed to in writing, software
164+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
165+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
166+# License for the specific language governing permissions and limitations
167+# under the License.
168+"""
169+ CLI interface for IPAM.
170+"""
171+import gettext
172+import optparse
173+import os
174+from os import environ as env
175+import sys
176+
177+# If ../melange/__init__.py exists, add ../ to Python search path, so that
178+# it will override what happens to be installed in /usr/(local/)lib/python...
179+possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
180+ os.pardir,
181+ os.pardir))
182+if os.path.exists(os.path.join(possible_topdir, 'melange', '__init__.py')):
183+ sys.path.insert(0, possible_topdir)
184+
185+gettext.install("melange", unicode=1)
186+
187+from melange import version
188+from melange.common.auth import KeystoneClient
189+from melange.common.client import HTTPClient
190+from melange.common.utils import MethodInspector
191+from melange.ipam.client import (IpBlockClient, SubnetClient, PolicyClient,
192+ UnusableIpOctetsClient,
193+ UnusableIpRangesClient)
194+
195+
196+def create_options(parser):
197+ """
198+ Sets up the CLI and config-file options that may be
199+ parsed and program commands.
200+
201+ :param parser: The option parser
202+ """
203+ parser.add_option('-v', '--verbose', default=False, action="store_true",
204+ help="Print more verbose output")
205+ parser.add_option('-H', '--host', metavar="ADDRESS", default="0.0.0.0",
206+ help="Address of Melange API host. "
207+ "Default: %default")
208+ parser.add_option('-p', '--port', dest="port", metavar="PORT",
209+ type=int, default=9898,
210+ help="Port the Melange API host listens on. "
211+ "Default: %default")
212+ parser.add_option('-t', '--tenant', dest="tenant", metavar="TENANT",
213+ type=str, help="tenant id in case of tenant resources")
214+ parser.add_option('--auth-token', dest="auth_token",
215+ metavar="MELANGE_AUTH_TOKEN",
216+ default=env.get('MELANGE_AUTH_TOKEN', None),
217+ type=str, help="Auth token received from keystone")
218+ parser.add_option('-u', '--username', dest="username",
219+ metavar="MELANGE_USERNAME",
220+ default=env.get('MELANGE_USERNAME', None),
221+ type=str, help="Melange user name")
222+ parser.add_option('-k', '--api-key', dest="api_key",
223+ metavar="MELANGE_API_KEY",
224+ default=env.get('MELANGE_API_KEY', None),
225+ type=str, help="Melange access key")
226+ parser.add_option('-a', '--auth-url', dest="auth_url",
227+ metavar="MELANGE_AUTH_URL", type=str,
228+ default=env.get('MELANGE_AUTH_URL', None),
229+ help="Url of keystone service")
230+ parser.add_option('--timeout', dest="timeout",
231+ metavar="MELANGE_TIME_OUT", type=int,
232+ default=env.get('MELANGE_TIME_OUT', None),
233+ help="timeout for melange client operations")
234+
235+
236+def parse_options(parser, cli_args):
237+ """
238+ Returns the parsed CLI options, command to run and its arguments, merged
239+ with any same-named options found in a configuration file
240+
241+ :param parser: The option parser
242+ """
243+ (options, args) = parser.parse_args(cli_args)
244+ if not args:
245+ parser.print_usage()
246+ sys.exit(2)
247+ return (options, args)
248+
249+
250+def usage():
251+ usage = """
252+%prog category action [args] [options]"
253+
254+Available categories:
255+
256+ """
257+ for k, _v in categories.iteritems():
258+ usage = usage + ("\t%s\n" % k)
259+ return usage
260+
261+
262+categories = dict(ip_block=IpBlockClient, subnet=SubnetClient,
263+ policy=PolicyClient, unusable_ip_range=UnusableIpRangesClient,
264+ unusable_ip_octet=UnusableIpOctetsClient)
265+
266+
267+def lookup(name, hash):
268+ result = hash.get(name, None)
269+ if not result:
270+ print "%s does not match any options:" % name
271+ print_keys(hash)
272+ sys.exit(2)
273+
274+ return result
275+
276+
277+def print_keys(hash):
278+ for k, _v in hash.iteritems():
279+ print "\t%s" % k
280+
281+
282+def methods_of(obj):
283+ """Get all callable methods of an object that don't start with underscore
284+ returns a list of tuples of the form (method_name, method)"""
285+
286+ def is_public_method(attr):
287+ return callable(getattr(obj, attr)) and not attr.startswith('_')
288+
289+ return dict((attr, getattr(obj, attr)) for attr in dir(obj)
290+ if is_public_method(attr))
291+
292+
293+def main():
294+ oparser = optparse.OptionParser(version='%%prog %s'
295+ % version.version_string(),
296+ usage=usage().strip())
297+ create_options(oparser)
298+ (options, args) = parse_options(oparser, sys.argv[1:])
299+
300+ script_name = os.path.basename(sys.argv[0])
301+ category = args.pop(0)
302+ http_client = HTTPClient(options.host, options.port, options.timeout)
303+ auth_client = KeystoneClient(options.auth_url, options.username,
304+ options.api_key, options.auth_token)
305+
306+ category_client_class = lookup(category, categories)
307+ client = category_client_class(http_client, auth_client, options.tenant)
308+ client_actions = methods_of(client)
309+ if len(args) < 1:
310+ print "Usage: " + script_name + " category action [<args>]"
311+ print _("Available actions for %s category:") % category
312+ print_keys(client_actions)
313+ sys.exit(2)
314+ action = args.pop(0)
315+ fn = lookup(action, client_actions)
316+
317+ # call the action with the remaining arguments
318+ try:
319+ print fn(*args)
320+ sys.exit(0)
321+ except TypeError:
322+ print _("Possible wrong number of arguments supplied")
323+ print "Usage: %s %s %s" % (script_name, category, MethodInspector(fn))
324+ if options.verbose:
325+ raise
326+ sys.exit(2)
327+ except Exception:
328+ print _("Command failed, please check log for more info")
329+ raise
330+
331+
332+if __name__ == '__main__':
333+ main()
334
335=== added file 'bin/melange-delete-deallocated-ips'
336--- bin/melange-delete-deallocated-ips 1970-01-01 00:00:00 +0000
337+++ bin/melange-delete-deallocated-ips 2011-08-25 11:33:54 +0000
338@@ -0,0 +1,57 @@
339+#!/usr/bin/env python
340+# vim: tabstop=4 shiftwidth=4 softtabstop=4
341+
342+# Copyright 2011 OpenStack LLC.
343+# All Rights Reserved.
344+#
345+# Licensed under the Apache License, Version 2.0 (the "License"); you may
346+# not use this file except in compliance with the License. You may obtain
347+# a copy of the License at
348+#
349+# http://www.apache.org/licenses/LICENSE-2.0
350+#
351+# Unless required by applicable law or agreed to in writing, software
352+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
353+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
354+# License for the specific language governing permissions and limitations
355+# under the License.
356+import gettext
357+import logging
358+import optparse
359+import os
360+import sys
361+
362+# If ../melange/__init__.py exists, add ../ to Python search path, so that
363+# it will override what happens to be installed in /usr/(local/)lib/python...
364+possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
365+ os.pardir,
366+ os.pardir))
367+if os.path.exists(os.path.join(possible_topdir, 'melange', '__init__.py')):
368+ sys.path.insert(0, possible_topdir)
369+
370+gettext.install('melange', unicode=1)
371+
372+from melange.db import db_api
373+from melange.common import config
374+from melange.ipam.models import IpBlock
375+
376+
377+def _configure_db_session(conf):
378+ db_api.configure_db(conf)
379+
380+
381+def _load_app_environment():
382+ oparser = optparse.OptionParser()
383+ config.add_log_options(oparser)
384+ (options, args) = config.parse_options(oparser)
385+ conf_file, conf = config.load_paste_config('melange', options, args)
386+ config.setup_logging(options=options, conf=conf)
387+ _configure_db_session(conf)
388+
389+
390+if __name__ == '__main__':
391+ try:
392+ _load_app_environment()
393+ IpBlock.delete_all_deallocated_ips()
394+ except RuntimeError, e:
395+ sys.exit("ERROR: %s" % e)
396
397=== added file 'bin/melange-manage'
398--- bin/melange-manage 1970-01-01 00:00:00 +0000
399+++ bin/melange-manage 2011-08-25 11:33:54 +0000
400@@ -0,0 +1,125 @@
401+#!/usr/bin/env python
402+# vim: tabstop=4 shiftwidth=4 softtabstop=4
403+
404+# Copyright 2011 OpenStack LLC.
405+# All Rights Reserved.
406+#
407+# Licensed under the Apache License, Version 2.0 (the "License"); you may
408+# not use this file except in compliance with the License. You may obtain
409+# a copy of the License at
410+#
411+# http://www.apache.org/licenses/LICENSE-2.0
412+#
413+# Unless required by applicable law or agreed to in writing, software
414+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
415+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
416+# License for the specific language governing permissions and limitations
417+# under the License.
418+import gettext
419+import optparse
420+import os
421+import re
422+import sys
423+import time
424+
425+# If ../melange/__init__.py exists, add ../ to Python search path, so that
426+# it will override what happens to be installed in /usr/(local/)lib/python...
427+possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
428+ os.pardir,
429+ os.pardir))
430+if os.path.exists(os.path.join(possible_topdir, 'melange', '__init__.py')):
431+ sys.path.insert(0, possible_topdir)
432+
433+gettext.install('melange', unicode=1)
434+
435+
436+from melange.common import config
437+from melange.common import wsgi
438+from melange.common.utils import MethodInspector
439+from melange.db import db_api
440+
441+
442+def create_options(parser):
443+ """
444+ Sets up the CLI and config-file options that may be
445+ parsed and program commands.
446+ :param parser: The option parser
447+ """
448+ parser.add_option('-p', '--port', dest="port", metavar="PORT",
449+ type=int, default=9898,
450+ help="Port the Melange API host listens on. "
451+ "Default: %default")
452+ config.add_common_options(parser)
453+ config.add_log_options(parser)
454+
455+
456+class Commands(object):
457+
458+ def __init__(self, conf):
459+ self.conf = conf
460+
461+ def db_sync(self):
462+ print self.conf
463+ db_api.db_sync(self.conf)
464+
465+ def db_upgrade(self, version=None):
466+ db_api.db_upgrade(self.conf, version)
467+
468+ def db_downgrade(self, version):
469+ db_api.db_downgrade(self.conf, version)
470+
471+ def execute(self, command_name, *args):
472+ if self.has(command_name):
473+ return getattr(self, command_name)(*args)
474+
475+ _commands = ['db_sync', 'db_upgrade', 'db_downgrade']
476+
477+ @classmethod
478+ def has(cls, command_name):
479+ return (command_name in cls._commands)
480+
481+ @classmethod
482+ def all(cls):
483+ return cls._commands
484+
485+ def params_of(self, command_name):
486+ if Commands.has(command_name):
487+ return MethodInspector(getattr(self, command_name))
488+
489+
490+def usage():
491+ usage = """
492+%prog action [args] [options]
493+
494+Available actions:
495+
496+ """
497+ for action in Commands.all():
498+ usage = usage + ("\t%s\n" % action)
499+ return usage
500+
501+
502+if __name__ == '__main__':
503+ oparser = optparse.OptionParser(version='%%prog VERSION', usage=usage())
504+ create_options(oparser)
505+ (options, args) = config.parse_options(oparser)
506+
507+ if len(args) < 1 or not Commands.has(args[0]):
508+ oparser.print_usage()
509+ sys.exit(2)
510+
511+ try:
512+ conf_file, conf = config.load_paste_config('melange', options, args)
513+ config.setup_logging(options, conf)
514+
515+ command_name = args.pop(0)
516+ Commands(conf).execute(command_name, *args)
517+ sys.exit(0)
518+ except TypeError as e:
519+ print _("Possible wrong number of arguments supplied")
520+ command_params = Commands(conf).params_of(command_name)
521+ print "Usage: melange-manage %s" % command_params
522+ sys.exit(2)
523+ except Exception as e:
524+ print _("Command failed, please check log for more info")
525+ raise
526
527=== modified file 'bin/nova-manage'
528--- bin/nova-manage 2011-08-24 21:01:33 +0000
529+++ bin/nova-manage 2011-08-25 11:33:54 +0000
530@@ -58,11 +58,11 @@
531 import json
532 import math
533 import netaddr
534+from optparse import OptionParser
535 import os
536 import sys
537 import time
538
539-from optparse import OptionParser
540
541 # If ../nova/__init__.py exists, add ../ to Python search path, so that
542 # it will override what happens to be installed in /usr/(local/)lib/python...
543
544=== added directory 'etc/melange'
545=== added file 'etc/melange/melange.conf.sample'
546--- etc/melange/melange.conf.sample 1970-01-01 00:00:00 +0000
547+++ etc/melange/melange.conf.sample 2011-08-25 11:33:54 +0000
548@@ -0,0 +1,77 @@
549+[DEFAULT]
550+# Show more verbose log output (sets INFO log level output)
551+verbose = True
552+
553+# Show debugging output in logs (sets DEBUG log level output)
554+debug = True
555+
556+# Address to bind the API server
557+bind_host = 0.0.0.0
558+
559+# Port the bind the API server to
560+bind_port = 9898
561+
562+# SQLAlchemy connection string for the reference implementation
563+# registry server. Any valid SQLAlchemy connection string is fine.
564+# See: http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/connections.html#sqlalchemy.create_engine
565+sql_connection = sqlite:///melange.sqlite
566+
567+# Period in seconds after which SQLAlchemy should reestablish its connection
568+# to the database.
569+#
570+# MySQL uses a default `wait_timeout` of 8 hours, after which it will drop
571+# idle connections. This can result in 'MySQL Gone Away' exceptions. If you
572+# notice this, you can lower this value to ensure that SQLAlchemy reconnects
573+# before MySQL can drop the connection.
574+sql_idle_timeout = 3600
575+
576+#DB Api Implementation
577+db_api_implementation = "melange.db.sqlalchemy.api"
578+
579+# Path to the extensions
580+api_extensions_path = melange/extensions
581+
582+# Cidr used for auto creating private ip block in a network
583+default_cidr = 10.0.0.0/24
584+
585+#IPV6 Generator Factory
586+#ipv6_generator=melange.ipv6.tenant_based_generator.TenantBasedIpV6Generator
587+
588+#DNS info for a data_center
589+dns1 = "ns1.example.com"
590+dns2 = "ns2.example.com"
591+
592+[composite:melange]
593+use = call:melange.common.wsgi:versioned_urlmap
594+/: versions
595+/v0.1: melangeapi
596+
597+[app:versions]
598+paste.app_factory = melange.versions:app_factory
599+
600+[pipeline:melangeapi]
601+pipeline = extensions melangeapp
602+
603+[filter:extensions]
604+paste.filter_factory = melange.common.extensions:ExtensionMiddleware.factory
605+
606+[filter:tokenauth]
607+paste.filter_factory = keystone.middleware.auth_token:filter_factory
608+service_protocol = http
609+service_host = 127.0.0.1
610+service_port = 808
611+auth_host = 127.0.0.1
612+auth_port = 5001
613+auth_protocol = http
614+admin_token = 999888777666
615+
616+[filter:authorization]
617+paste.filter_factory = melange.common.auth:AuthorizationMiddleware.factory
618+url_auth_factory = melange.ipam.service.UrlAuthorizationFactory
619+
620+[app:melangeapp]
621+paste.app_factory = melange.ipam.service:app_factory
622+
623+#Add this filter to log request and response for debugging
624+[filter:debug]
625+paste.filter_factory = melange.common.wsgi:Debug.factory
626
627=== added file 'etc/melange/melange.conf.test'
628--- etc/melange/melange.conf.test 1970-01-01 00:00:00 +0000
629+++ etc/melange/melange.conf.test 2011-08-25 11:33:54 +0000
630@@ -0,0 +1,61 @@
631+[DEFAULT]
632+# Show more verbose log output (sets INFO log level output)
633+verbose = False
634+
635+# Show debugging output in logs (sets DEBUG log level output)
636+debug = False
637+
638+# Path to the extensions
639+api_extensions_path = ../../melange/tests/unit/extensions
640+
641+# Address to bind the API server
642+bind_host = 0.0.0.0
643+
644+# Port the bind the API server to
645+bind_port = 9898
646+
647+# SQLAlchemy connection string for the reference implementation
648+# registry server. Any valid SQLAlchemy connection string is fine.
649+# See: http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/connections.html#sqlalchemy.create_engine
650+sql_connection = sqlite:///melange_test.sqlite
651+
652+# Period in seconds after which SQLAlchemy should reestablish its connection
653+# to the database.
654+#
655+# MySQL uses a default `wait_timeout` of 8 hours, after which it will drop
656+# idle connections. This can result in 'MySQL Gone Away' exceptions. If you
657+# notice this, you can lower this value to ensure that SQLAlchemy reconnects
658+# before MySQL can drop the connection.
659+sql_idle_timeout = 3600
660+
661+#DB Api Implementation
662+db_api_implementation = "melange.db.sqlalchemy.api"
663+
664+# Cidr used for auto creating private ip block in a network
665+default_cidr = 10.0.0.0/24
666+
667+#IPV6 Generator Factory
668+#ipv6_generator=melange.ipv6.tenant_based_generator.TenantBasedIpV6Generator
669+
670+#DNS info for a data_center
671+nameserver = "ns.example.com"
672+
673+[pipeline:extensions_app_with_filter]
674+pipeline = extensions extensions_test_app
675+
676+[filter:extensions]
677+paste.filter_factory = melange.common.extensions:ExtensionMiddleware.factory
678+
679+[app:extensions_test_app]
680+paste.app_factory = melange.tests.unit.test_extensions:app_factory
681+
682+[composite:versioned_melange]
683+use = egg:Paste#urlmap
684+/: versions
685+/v0.1: melange
686+
687+[app:versions]
688+paste.app_factory = melange.versions:app_factory
689+
690+[app:melange]
691+paste.app_factory = melange.ipam.service:app_factory
692
693=== added directory 'melange'
694=== added file 'melange/README'
695--- melange/README 1970-01-01 00:00:00 +0000
696+++ melange/README 2011-08-25 11:33:54 +0000
697@@ -0,0 +1,14 @@
698+# Running Melange Tests
699+Use the run_tests.sh from the root nova folder. It doesnt run Melange tests by default.
700+1) Create the required databases:
701+>mysql -uroot -p -e "CREATE DATABASE melange_test;CREATE DATABASE melange"
702+2) To run melange tests:
703+>run_tests.sh melange
704+>run_tests.sh melange.tests.unit
705+>run_tests.sh melange.tests.functional
706+
707+# Running Melange App
708+>mysql -uroot -p -e "CREATE DATABASE melange"
709+>cd <nova_root>
710+>cp etc/melange/melange.conf.sample ~/melange.conf
711+>bin/melange
712
713=== added file 'melange/__init__.py'
714--- melange/__init__.py 1970-01-01 00:00:00 +0000
715+++ melange/__init__.py 2011-08-25 11:33:54 +0000
716@@ -0,0 +1,29 @@
717+# vim: tabstop=4 shiftwidth=4 softtabstop=4
718+
719+# Copyright 2011 OpenStack LLC.
720+# All Rights Reserved.
721+#
722+# Licensed under the Apache License, Version 2.0 (the "License"); you may
723+# not use this file except in compliance with the License. You may obtain
724+# a copy of the License at
725+#
726+# http://www.apache.org/licenses/LICENSE-2.0
727+#
728+# Unless required by applicable law or agreed to in writing, software
729+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
730+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
731+# License for the specific language governing permissions and limitations
732+# under the License.
733+import os
734+
735+
736+def melange_root_path():
737+ return os.path.dirname(__file__)
738+
739+
740+def melange_bin_path(filename="."):
741+ return os.path.join(melange_root_path(), "..", "bin", filename)
742+
743+
744+def melange_etc_path(filename="."):
745+ return os.path.join(melange_root_path(), "..", "etc", "melange", filename)
746
747=== added directory 'melange/common'
748=== added file 'melange/common/__init__.py'
749--- melange/common/__init__.py 1970-01-01 00:00:00 +0000
750+++ melange/common/__init__.py 2011-08-25 11:33:54 +0000
751@@ -0,0 +1,16 @@
752+# vim: tabstop=4 shiftwidth=4 softtabstop=4
753+
754+# Copyright 2010-2011 OpenStack LLC.
755+# All Rights Reserved.
756+#
757+# Licensed under the Apache License, Version 2.0 (the "License"); you may
758+# not use this file except in compliance with the License. You may obtain
759+# a copy of the License at
760+#
761+# http://www.apache.org/licenses/LICENSE-2.0
762+#
763+# Unless required by applicable law or agreed to in writing, software
764+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
765+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
766+# License for the specific language governing permissions and limitations
767+# under the License.
768
769=== added file 'melange/common/auth.py'
770--- melange/common/auth.py 1970-01-01 00:00:00 +0000
771+++ melange/common/auth.py 2011-08-25 11:33:54 +0000
772@@ -0,0 +1,97 @@
773+# vim: tabstop=4 shiftwidth=4 softtabstop=4
774+
775+# Copyright 2011 OpenStack LLC.
776+# All Rights Reserved.
777+#
778+# Licensed under the Apache License, Version 2.0 (the "License"); you may
779+# not use this file except in compliance with the License. You may obtain
780+# a copy of the License at
781+#
782+# http://www.apache.org/licenses/LICENSE-2.0
783+#
784+# Unless required by applicable law or agreed to in writing, software
785+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
786+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
787+# License for the specific language governing permissions and limitations
788+# under the License.
789+import httplib2
790+import json
791+import re
792+from webob.exc import HTTPForbidden
793+import wsgi
794+
795+from melange.common.utils import import_class
796+
797+
798+class AuthorizationMiddleware(wsgi.Middleware):
799+
800+ def __init__(self, application, *auth_providers, **local_config):
801+ self.auth_providers = auth_providers
802+ super(AuthorizationMiddleware, self).__init__(application,
803+ **local_config)
804+
805+ def process_request(self, request):
806+ roles = request.headers.get('X_ROLE', '').split(',')
807+ tenant_id = request.headers.get('X_TENANT', None)
808+ for provider in self.auth_providers:
809+ provider.authorize(request, tenant_id, roles)
810+
811+ @classmethod
812+ def factory(cls, global_config, **local_config):
813+ def _factory(app):
814+ url_auth_factory = import_class(local_config['url_auth_factory'])
815+ return cls(app, url_auth_factory(), TenantBasedAuth(),
816+ **local_config)
817+ return _factory
818+
819+
820+class TenantBasedAuth(object):
821+ tenant_scoped_url = re.compile(".*/tenants/(?P<tenant_id>.*?)/.*")
822+
823+ def authorize(self, request, tenant_id, roles):
824+ if('Admin' in roles):
825+ return True
826+ match = self.tenant_scoped_url.match(request.path_info)
827+ if match and tenant_id != match.group('tenant_id'):
828+ raise HTTPForbidden(_("User with tenant id %s cannot access "
829+ "this resource") % tenant_id)
830+ return True
831+
832+
833+class RoleBasedAuth(object):
834+
835+ def __init__(self, mapper):
836+ self.mapper = mapper
837+
838+ def authorize(self, request, tenant_id, roles):
839+ if('Admin' in roles):
840+ return True
841+ match = self.mapper.match(request.path_info, request.environ)
842+ if match and match['action'] in match['controller'].admin_actions:
843+ raise HTTPForbidden(_("User with roles %s cannot access "
844+ "admin actions") % ', '.join(roles))
845+ return True
846+
847+
848+class KeystoneClient(httplib2.Http):
849+
850+ def __init__(self, url, username, access_key, auth_token=None):
851+ super(KeystoneClient, self).__init__()
852+ self.url = str(url) + "/v2.0/tokens"
853+ self.username = username
854+ self.access_key = access_key
855+ self.auth_token = auth_token
856+
857+ def get_token(self):
858+ if self.auth_token:
859+ return self.auth_token
860+ headers = {'content-type': 'application/json'}
861+ request_body = json.dumps({"passwordCredentials":
862+ {"username": self.username,
863+ 'password': self.access_key}})
864+ res, body = self.request(self.url, "POST", headers=headers,
865+ body=request_body)
866+ if int(res.status) >= 400:
867+ raise Exception(_("Error occured while retrieving token : %s")
868+ % body)
869+ return json.loads(body)['auth']['token']['id']
870
871=== added file 'melange/common/client.py'
872--- melange/common/client.py 1970-01-01 00:00:00 +0000
873+++ melange/common/client.py 2011-08-25 11:33:54 +0000
874@@ -0,0 +1,64 @@
875+# vim: tabstop=4 shiftwidth=4 softtabstop=4
876+
877+# Copyright 2010 OpenStack LLC.
878+# All Rights Reserved.
879+#
880+# Licensed under the Apache License, Version 2.0 (the "License"); you may
881+# not use this file except in compliance with the License. You may obtain
882+# a copy of the License at
883+#
884+# http://www.apache.org/licenses/LICENSE-2.0
885+#
886+# Unless required by applicable law or agreed to in writing, software
887+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
888+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
889+# License for the specific language governing permissions and limitations
890+# under the License.
891+import httplib
892+import socket
893+import urllib
894+
895+
896+class HTTPClient(object):
897+
898+ def __init__(self, host='localhost', port=8080, use_ssl=False, timeout=60):
899+ self.host = host
900+ self.port = port
901+ self.use_ssl = use_ssl
902+ self.timeout = timeout
903+
904+ def get(self, path, params=None, headers=None):
905+ params = params or {}
906+ headers = headers or {}
907+ return self.do_request("GET", path, params=params, headers=headers)
908+
909+ def post(self, path, body=None, headers=None):
910+ headers = headers or {}
911+ return self.do_request("POST", path, body=body, headers=headers)
912+
913+ def delete(self, path, headers=None):
914+ headers = headers or {}
915+ return self.do_request("DELETE", path, headers=headers)
916+
917+ def _get_connection(self):
918+ if self.use_ssl:
919+ return httplib.HTTPSConnection(self.host, self.port,
920+ timeout=self.timeout)
921+ else:
922+ return httplib.HTTPConnection(self.host, self.port,
923+ timeout=self.timeout)
924+
925+ def do_request(self, method, path, body=None, headers=None, params=None):
926+ params = params or {}
927+ headers = headers or {}
928+
929+ url = path + '?' + urllib.urlencode(params)
930+
931+ try:
932+ connection = self._get_connection()
933+ connection.request(method, url, body, headers)
934+ response = connection.getresponse()
935+ return response
936+ except (socket.error, IOError), e:
937+ raise Exception(_("Unable to connect to "
938+ "server. Got error: %s") % e)
939
940=== added file 'melange/common/config.py'
941--- melange/common/config.py 1970-01-01 00:00:00 +0000
942+++ melange/common/config.py 2011-08-25 11:33:54 +0000
943@@ -0,0 +1,36 @@
944+#!/usr/bin/env python
945+# vim: tabstop=4 shiftwidth=4 softtabstop=4
946+
947+# Copyright 2011 OpenStack LLC.
948+# All Rights Reserved.
949+#
950+# Licensed under the Apache License, Version 2.0 (the "License"); you may
951+# not use this file except in compliance with the License. You may obtain
952+# a copy of the License at
953+#
954+# http://www.apache.org/licenses/LICENSE-2.0
955+#
956+# Unless required by applicable law or agreed to in writing, software
957+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
958+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
959+# License for the specific language governing permissions and limitations
960+# under the License.
961+
962+"""
963+Routines for configuring Melange
964+"""
965+from openstack.common.config import (parse_options,
966+ add_log_options,
967+ add_common_options,
968+ load_paste_config,
969+ setup_logging,
970+ load_paste_app, get_option)
971+
972+
973+class Config(object):
974+
975+ instance = {}
976+
977+ @classmethod
978+ def get(cls, key, default=None):
979+ return cls.instance.get(key, default)
980
981=== added file 'melange/common/exception.py'
982--- melange/common/exception.py 1970-01-01 00:00:00 +0000
983+++ melange/common/exception.py 2011-08-25 11:33:54 +0000
984@@ -0,0 +1,34 @@
985+# vim: tabstop=4 shiftwidth=4 softtabstop=4
986+
987+# Copyright 2011 OpenStack LLC.
988+# All Rights Reserved.
989+#
990+# Licensed under the Apache License, Version 2.0 (the "License"); you may
991+# not use this file except in compliance with the License. You may obtain
992+# a copy of the License at
993+#
994+# http://www.apache.org/licenses/LICENSE-2.0
995+#
996+# Unless required by applicable law or agreed to in writing, software
997+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
998+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
999+# License for the specific language governing permissions and limitations
1000+# under the License.
1001+
1002+"""
1003+Nova base exception handling, including decorator for re-raising
1004+Nova-type exceptions. SHOULD include dedicated exception logging.
1005+"""
1006+
1007+from openstack.common.exception import (Error, ProcessExecutionError,
1008+ DatabaseMigrationError,
1009+ InvalidContentType)
1010+
1011+
1012+class MelangeError(Error):
1013+
1014+ def __init__(self, message=None):
1015+ super(MelangeError, self).__init__(message or self._error_message())
1016+
1017+ def _error_message(self):
1018+ pass
1019
1020=== added file 'melange/common/extensions.py'
1021--- melange/common/extensions.py 1970-01-01 00:00:00 +0000
1022+++ melange/common/extensions.py 2011-08-25 11:33:54 +0000
1023@@ -0,0 +1,437 @@
1024+# vim: tabstop=4 shiftwidth=4 softtabstop=4
1025+
1026+# Copyright 2011 OpenStack LLC.
1027+# Copyright 2011 Justin Santa Barbara
1028+# All Rights Reserved.
1029+#
1030+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1031+# not use this file except in compliance with the License. You may obtain
1032+# a copy of the License at
1033+#
1034+# http://www.apache.org/licenses/LICENSE-2.0
1035+#
1036+# Unless required by applicable law or agreed to in writing, software
1037+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1038+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1039+# License for the specific language governing permissions and limitations
1040+# under the License.
1041+import imp
1042+import logging
1043+import os
1044+import routes
1045+import webob.dec
1046+import webob.exc
1047+
1048+from melange.common import exception
1049+from melange.common import wsgi
1050+
1051+
1052+LOG = logging.getLogger('melange.common.extensions')
1053+
1054+
1055+class ExtensionDescriptor(object):
1056+ """Base class that defines the contract for extensions.
1057+
1058+ Note that you don't have to derive from this class to have a valid
1059+ extension; it is purely a convenience.
1060+
1061+ """
1062+
1063+ def get_name(self):
1064+ """The name of the extension.
1065+
1066+ e.g. 'Fox In Socks'
1067+
1068+ """
1069+ raise NotImplementedError()
1070+
1071+ def get_alias(self):
1072+ """The alias for the extension.
1073+
1074+ e.g. 'FOXNSOX'
1075+
1076+ """
1077+ raise NotImplementedError()
1078+
1079+ def get_description(self):
1080+ """Friendly description for the extension.
1081+
1082+ e.g. 'The Fox In Socks Extension'
1083+
1084+ """
1085+ raise NotImplementedError()
1086+
1087+ def get_namespace(self):
1088+ """The XML namespace for the extension.
1089+
1090+ e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0'
1091+
1092+ """
1093+ raise NotImplementedError()
1094+
1095+ def get_updated(self):
1096+ """The timestamp when the extension was last updated.
1097+
1098+ e.g. '2011-01-22T13:25:27-06:00'
1099+
1100+ """
1101+ # NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS
1102+ raise NotImplementedError()
1103+
1104+ def get_resources(self):
1105+ """List of extensions.ResourceExtension extension objects.
1106+
1107+ Resources define new nouns, and are accessible through URLs.
1108+
1109+ """
1110+ resources = []
1111+ return resources
1112+
1113+ def get_actions(self):
1114+ """List of extensions.ActionExtension extension objects.
1115+
1116+ Actions are verbs callable from the API.
1117+
1118+ """
1119+ actions = []
1120+ return actions
1121+
1122+ def get_request_extensions(self):
1123+ """List of extensions.RequestException extension objects.
1124+
1125+ Request extensions are used to handle custom request data.
1126+
1127+ """
1128+ request_exts = []
1129+ return request_exts
1130+
1131+
1132+class ActionExtensionController(wsgi.Controller):
1133+
1134+ def __init__(self, application):
1135+
1136+ self.application = application
1137+ self.action_handlers = {}
1138+
1139+ def add_action(self, action_name, handler):
1140+ self.action_handlers[action_name] = handler
1141+
1142+ def action(self, request, id):
1143+
1144+ input_dict = self._deserialize(request.body,
1145+ request.get_content_type())
1146+ for action_name, handler in self.action_handlers.iteritems():
1147+ if action_name in input_dict:
1148+ return handler(input_dict, request, id)
1149+ # no action handler found (bump to downstream application)
1150+ response = self.application
1151+ return response
1152+
1153+
1154+class RequestExtensionController(wsgi.Controller):
1155+
1156+ def __init__(self, application):
1157+ self.application = application
1158+ self.handlers = []
1159+
1160+ def add_handler(self, handler):
1161+ self.handlers.append(handler)
1162+
1163+ def process(self, request, *args, **kwargs):
1164+ res = request.get_response(self.application)
1165+ # currently request handlers are un-ordered
1166+ for handler in self.handlers:
1167+ res = handler(request, res)
1168+ return res
1169+
1170+
1171+class ExtensionController(wsgi.Controller):
1172+
1173+ def __init__(self, extension_manager):
1174+ self.extension_manager = extension_manager
1175+
1176+ def _translate(self, ext):
1177+ ext_data = {}
1178+ ext_data['name'] = ext.get_name()
1179+ ext_data['alias'] = ext.get_alias()
1180+ ext_data['description'] = ext.get_description()
1181+ ext_data['namespace'] = ext.get_namespace()
1182+ ext_data['updated'] = ext.get_updated()
1183+ ext_data['links'] = [] # TODO(dprince): implement extension links
1184+ return ext_data
1185+
1186+ def index(self, request):
1187+ extensions = []
1188+ for _alias, ext in self.extension_manager.extensions.iteritems():
1189+ extensions.append(self._translate(ext))
1190+ return dict(extensions=extensions)
1191+
1192+ def show(self, request, id):
1193+ # NOTE(dprince): the extensions alias is used as the 'id' for show
1194+ ext = self.extension_manager.extensions[id]
1195+ return self._translate(ext)
1196+
1197+ def delete(self, request, id):
1198+ raise webob.exc.HTTPNotFound()
1199+
1200+ def create(self, request):
1201+ raise webob.exc.HTTPNotFound()
1202+
1203+
1204+class ExtensionMiddleware(wsgi.Middleware):
1205+ """Extensions middleware for WSGI."""
1206+ @classmethod
1207+ def factory(cls, global_config, **local_config):
1208+ """Paste factory."""
1209+ def _factory(app):
1210+ return cls(app, global_config, **local_config)
1211+ return _factory
1212+
1213+ def _action_ext_controllers(self, application, ext_mgr, mapper):
1214+ """Return a dict of ActionExtensionController-s by collection."""
1215+ action_controllers = {}
1216+ for action in ext_mgr.get_actions():
1217+ if not action.collection in action_controllers.keys():
1218+ controller = ActionExtensionController(application)
1219+ mapper.connect("/%s/:(id)/action.:(format)" %
1220+ action.collection,
1221+ action='action',
1222+ controller=controller,
1223+ conditions=dict(method=['POST']))
1224+ mapper.connect("/%s/:(id)/action" % action.collection,
1225+ action='action',
1226+ controller=controller,
1227+ conditions=dict(method=['POST']))
1228+ action_controllers[action.collection] = controller
1229+
1230+ return action_controllers
1231+
1232+ def _request_ext_controllers(self, application, ext_mgr, mapper):
1233+ """Returns a dict of RequestExtensionController-s by collection."""
1234+ request_ext_controllers = {}
1235+ for req_ext in ext_mgr.get_request_extensions():
1236+ if not req_ext.key in request_ext_controllers.keys():
1237+ controller = RequestExtensionController(application)
1238+ mapper.connect(req_ext.url_route + '.:(format)',
1239+ action='process',
1240+ controller=controller,
1241+ conditions=req_ext.conditions)
1242+
1243+ mapper.connect(req_ext.url_route,
1244+ action='process',
1245+ controller=controller,
1246+ conditions=req_ext.conditions)
1247+ request_ext_controllers[req_ext.key] = controller
1248+
1249+ return request_ext_controllers
1250+
1251+ def __init__(self, application, config_params,
1252+ ext_mgr=None):
1253+ self.ext_mgr = (ext_mgr
1254+ or ExtensionManager(config_params.get('api_extensions_path',
1255+ '')))
1256+
1257+ mapper = routes.Mapper()
1258+
1259+ # extended resources
1260+ for resource in self.ext_mgr.get_resources():
1261+ LOG.debug(_('Extended resource: %s'),
1262+ resource.collection)
1263+ mapper.resource(resource.collection, resource.collection,
1264+ controller=resource.controller,
1265+ collection=resource.collection_actions,
1266+ member=resource.member_actions,
1267+ parent_resource=resource.parent)
1268+
1269+ # extended actions
1270+ action_controllers = self._action_ext_controllers(application,
1271+ self.ext_mgr, mapper)
1272+ for action in self.ext_mgr.get_actions():
1273+ LOG.debug(_('Extended action: %s'), action.action_name)
1274+ controller = action_controllers[action.collection]
1275+ controller.add_action(action.action_name, action.handler)
1276+
1277+ # extended requests
1278+ req_controllers = self._request_ext_controllers(application,
1279+ self.ext_mgr, mapper)
1280+ for request_ext in self.ext_mgr.get_request_extensions():
1281+ LOG.debug(_('Extended request: %s'), request_ext.key)
1282+ controller = req_controllers[request_ext.key]
1283+ controller.add_handler(request_ext.handler)
1284+
1285+ self._router = routes.middleware.RoutesMiddleware(self._dispatch,
1286+ mapper)
1287+
1288+ super(ExtensionMiddleware, self).__init__(application)
1289+
1290+ @webob.dec.wsgify(RequestClass=wsgi.Request)
1291+ def __call__(self, request):
1292+ """Route the incoming request with router."""
1293+ request.environ['extended.app'] = self.application
1294+ return self._router
1295+
1296+ @staticmethod
1297+ @webob.dec.wsgify(RequestClass=wsgi.Request)
1298+ def _dispatch(request):
1299+ """Dispatch the request.
1300+
1301+ Returns the routed WSGI app's response or defers to the extended
1302+ application.
1303+
1304+ """
1305+ match = request.environ['wsgiorg.routing_args'][1]
1306+ if not match:
1307+ return request.environ['extended.app']
1308+ app = match['controller']
1309+ return app
1310+
1311+
1312+class ExtensionManager(object):
1313+ """Load extensions from the configured extension path.
1314+
1315+ See melange/tests/unit/extensions/foxinsocks.py for an
1316+ example extension implementation.
1317+
1318+ """
1319+
1320+ def __init__(self, path):
1321+ LOG.info(_('Initializing extension manager.'))
1322+
1323+ self.path = path
1324+ self.extensions = {}
1325+ self._load_all_extensions()
1326+
1327+ def get_resources(self):
1328+ """Returns a list of ResourceExtension objects."""
1329+ resources = []
1330+ resources.append(ResourceExtension('extensions',
1331+ ExtensionController(self)))
1332+ for alias, ext in self.extensions.iteritems():
1333+ try:
1334+ resources.extend(ext.get_resources())
1335+ except AttributeError:
1336+ # NOTE(dprince): Extension aren't required to have resource
1337+ # extensions
1338+ pass
1339+ return resources
1340+
1341+ def get_actions(self):
1342+ """Returns a list of ActionExtension objects."""
1343+ actions = []
1344+ for alias, ext in self.extensions.iteritems():
1345+ try:
1346+ actions.extend(ext.get_actions())
1347+ except AttributeError:
1348+ # NOTE(dprince): Extension aren't required to have action
1349+ # extensions
1350+ pass
1351+ return actions
1352+
1353+ def get_request_extensions(self):
1354+ """Returns a list of RequestExtension objects."""
1355+ request_exts = []
1356+ for alias, ext in self.extensions.iteritems():
1357+ try:
1358+ request_exts.extend(ext.get_request_extensions())
1359+ except AttributeError:
1360+ # NOTE(dprince): Extension aren't required to have request
1361+ # extensions
1362+ pass
1363+ return request_exts
1364+
1365+ def _check_extension(self, extension):
1366+ """Checks for required methods in extension objects."""
1367+ try:
1368+ LOG.debug(_('Ext name: %s'), extension.get_name())
1369+ LOG.debug(_('Ext alias: %s'), extension.get_alias())
1370+ LOG.debug(_('Ext description: %s'), extension.get_description())
1371+ LOG.debug(_('Ext namespace: %s'), extension.get_namespace())
1372+ LOG.debug(_('Ext updated: %s'), extension.get_updated())
1373+ except AttributeError as ex:
1374+ LOG.exception(_("Exception loading extension: %s"), unicode(ex))
1375+
1376+ def _load_all_extensions(self):
1377+ """Load extensions from the configured path.
1378+
1379+ Load extensions from the configured path. The extension name is
1380+ constructed from the module_name. If your extension module was named
1381+ widgets.py the extension class within that module should be
1382+ 'Widgets'.
1383+
1384+ In addition, extensions are loaded from the 'contrib' directory.
1385+
1386+ See melange/tests/unit/extensions/foxinsocks.py for an example
1387+ extension implementation.
1388+
1389+ """
1390+ if os.path.exists(self.path):
1391+ self._load_all_extensions_from_path(self.path)
1392+
1393+ contrib_path = os.path.join(os.path.dirname(__file__), "contrib")
1394+ if os.path.exists(contrib_path):
1395+ self._load_all_extensions_from_path(contrib_path)
1396+
1397+ def _load_all_extensions_from_path(self, path):
1398+ for f in os.listdir(path):
1399+ LOG.info(_('Loading extension file: %s'), f)
1400+ mod_name, file_ext = os.path.splitext(os.path.split(f)[-1])
1401+ ext_path = os.path.join(path, f)
1402+ if file_ext.lower() == '.py' and not mod_name.startswith('_'):
1403+ mod = imp.load_source(mod_name, ext_path)
1404+ ext_name = mod_name[0].upper() + mod_name[1:]
1405+ new_ext_class = getattr(mod, ext_name, None)
1406+ if not new_ext_class:
1407+ LOG.warn(_('Did not find expected name '
1408+ '"%(ext_name)s" in %(file)s'),
1409+ {'ext_name': ext_name,
1410+ 'file': ext_path})
1411+ continue
1412+ new_ext = new_ext_class()
1413+ self._check_extension(new_ext)
1414+ self._add_extension(new_ext)
1415+
1416+ def _add_extension(self, ext):
1417+ alias = ext.get_alias()
1418+ LOG.info(_('Loaded extension: %s'), alias)
1419+
1420+ self._check_extension(ext)
1421+
1422+ if alias in self.extensions:
1423+ raise exception.MelangeError(_("Found duplicate extension: %s")
1424+ % alias)
1425+ self.extensions[alias] = ext
1426+
1427+
1428+class RequestExtension(object):
1429+ """Extend requests and responses of core melange OpenStack API controllers.
1430+
1431+ Provide a way to add data to responses and handle custom request data
1432+ that is sent to core melange OpenStack API controllers.
1433+
1434+ """
1435+ def __init__(self, method, url_route, handler):
1436+ self.url_route = url_route
1437+ self.handler = handler
1438+ self.conditions = dict(method=[method])
1439+ self.key = "%s-%s" % (method, url_route)
1440+
1441+
1442+class ActionExtension(object):
1443+ """Add custom actions to core melange OpenStack API controllers."""
1444+
1445+ def __init__(self, collection, action_name, handler):
1446+ self.collection = collection
1447+ self.action_name = action_name
1448+ self.handler = handler
1449+
1450+
1451+class ResourceExtension(object):
1452+ """Add top level resources to the OpenStack API in melange."""
1453+
1454+ def __init__(self, collection, controller, parent=None,
1455+ collection_actions={}, member_actions={}):
1456+ self.collection = collection
1457+ self.controller = controller
1458+ self.parent = parent
1459+ self.collection_actions = collection_actions
1460+ self.member_actions = member_actions
1461
1462=== added file 'melange/common/pagination.py'
1463--- melange/common/pagination.py 1970-01-01 00:00:00 +0000
1464+++ melange/common/pagination.py 2011-08-25 11:33:54 +0000
1465@@ -0,0 +1,108 @@
1466+# vim: tabstop=4 shiftwidth=4 softtabstop=4
1467+
1468+# Copyright 2011 OpenStack LLC.
1469+# All Rights Reserved.
1470+#
1471+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1472+# not use this file except in compliance with the License. You may obtain
1473+# a copy of the License at
1474+#
1475+# http://www.apache.org/licenses/LICENSE-2.0
1476+#
1477+# Unless required by applicable law or agreed to in writing, software
1478+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1479+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1480+# License for the specific language governing permissions and limitations
1481+# under the License.
1482+import urllib
1483+import urlparse
1484+from xml.dom import minidom
1485+
1486+from melange.common.utils import merge_dicts
1487+from melange.common.wsgi import Result
1488+
1489+
1490+class AtomLink(object):
1491+ """An atom link"""
1492+
1493+ def __init__(self, rel, href, link_type=None, hreflang=None, title=None):
1494+ self.rel = rel
1495+ self.href = href
1496+ self.link_type = link_type
1497+ self.hreflang = hreflang
1498+ self.title = title
1499+
1500+ def to_xml(self):
1501+ ATOM_NAMESPACE = "http://www.w3.org/2005/Atom"
1502+ doc = minidom.Document()
1503+ atom_elem = doc.createElementNS(ATOM_NAMESPACE, "link")
1504+ if self.link_type:
1505+ atom_elem.setAttribute("link_type", self.link_type)
1506+ if self.hreflang:
1507+ atom_elem.setAttribute("hreflang", self.hreflang)
1508+ if self.title:
1509+ atom_elem.setAttribute("title", self.title)
1510+ atom_elem.setAttribute("rel", self.rel)
1511+ atom_elem.setAttribute("href", self.href)
1512+ return atom_elem
1513+
1514+
1515+class PaginatedResult(Result):
1516+
1517+ def serialize_data(self, serializer, serialization_type):
1518+ data = self.data.data_for_json()
1519+ if serialization_type == "application/xml":
1520+ data = self.data.data_for_xml()
1521+ return serializer.serialize(data, serialization_type)
1522+
1523+
1524+class PaginatedDataView(object):
1525+
1526+ def __init__(self, collection_type, collection, current_page_url,
1527+ next_page_marker=None):
1528+ self.collection_type = collection_type
1529+ self.collection = collection
1530+ self.current_page_url = current_page_url
1531+ self.next_page_marker = next_page_marker
1532+
1533+ def data_for_json(self):
1534+ links_dict = {}
1535+ if self._links():
1536+ links_key = self.collection_type + "_links"
1537+ links_dict[links_key] = self._links()
1538+ return merge_dicts({self.collection_type: self.collection}, links_dict)
1539+
1540+ def data_for_xml(self):
1541+ atom_links = [AtomLink(link['rel'], link['href'])
1542+ for link in self._links()]
1543+ return {self.collection_type: self.collection + atom_links}
1544+
1545+ def _create_link(self, marker):
1546+ app_url = AppUrl(self.current_page_url)
1547+ return str(app_url.change_query_params(marker=marker))
1548+
1549+ def _links(self):
1550+ if not self.next_page_marker:
1551+ return []
1552+ next_link = dict(rel='next',
1553+ href=self._create_link(self.next_page_marker))
1554+ return [next_link]
1555+
1556+
1557+class AppUrl(object):
1558+
1559+ def __init__(self, url):
1560+ self.url = url
1561+
1562+ def __str__(self):
1563+ return self.url
1564+
1565+ def change_query_params(self, **kwargs):
1566+ parsed_url = urlparse.urlparse(self.url)
1567+ query_params = dict(urlparse.parse_qsl(parsed_url.query))
1568+ new_query_params = urllib.urlencode(merge_dicts(query_params, kwargs))
1569+ return self.__class__(
1570+ urlparse.ParseResult(parsed_url.scheme,
1571+ parsed_url.netloc, parsed_url.path,
1572+ parsed_url.params, new_query_params,
1573+ parsed_url.fragment).geturl())
1574
1575=== added file 'melange/common/utils.py'
1576--- melange/common/utils.py 1970-01-01 00:00:00 +0000
1577+++ melange/common/utils.py 2011-08-25 11:33:54 +0000
1578@@ -0,0 +1,171 @@
1579+# vim: tabstop=4 shiftwidth=4 softtabstop=4
1580+
1581+# Copyright 2011 OpenStack LLC.
1582+# All Rights Reserved.
1583+#
1584+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1585+# not use this file except in compliance with the License. You may obtain
1586+# a copy of the License at
1587+#
1588+# http://www.apache.org/licenses/LICENSE-2.0
1589+#
1590+# Unless required by applicable law or agreed to in writing, software
1591+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1592+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1593+# License for the specific language governing permissions and limitations
1594+# under the License.
1595+
1596+"""
1597+System-level utilities and helper functions.
1598+"""
1599+
1600+import datetime
1601+import inspect
1602+import logging
1603+import os
1604+import subprocess
1605+import uuid
1606+
1607+from openstack.common.utils import import_class, import_object
1608+
1609+from melange.common.exception import ProcessExecutionError
1610+
1611+
1612+def parse_int(subject):
1613+ try:
1614+ return int(subject)
1615+ except (ValueError, TypeError):
1616+ return None
1617+
1618+
1619+def execute(cmd, process_input=None, addl_env=None, check_exit_code=True):
1620+ logging.debug("Running cmd: %s", cmd)
1621+ env = os.environ.copy()
1622+ if addl_env:
1623+ env.update(addl_env)
1624+ obj = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
1625+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
1626+ result = None
1627+ if process_input != None:
1628+ result = obj.communicate(process_input)
1629+ else:
1630+ result = obj.communicate()
1631+ obj.stdin.close()
1632+ if obj.returncode:
1633+ logging.debug("Result was %s" % (obj.returncode))
1634+ if check_exit_code and obj.returncode != 0:
1635+ (stdout, stderr) = result
1636+ raise ProcessExecutionError(exit_code=obj.returncode,
1637+ stdout=stdout,
1638+ stderr=stderr,
1639+ cmd=cmd)
1640+ return result
1641+
1642+
1643+def utcnow():
1644+ return datetime.datetime.utcnow()
1645+
1646+
1647+def exclude(key_values, *exclude_keys):
1648+ return dict((key, value) for key, value in key_values.iteritems()
1649+ if key not in exclude_keys)
1650+
1651+
1652+def filter_dict(key_values, *include_keys):
1653+ return dict((key, value) for key, value in key_values.iteritems()
1654+ if key in include_keys)
1655+
1656+
1657+def stringify_keys(dictionary):
1658+ return dict((str(key), value) for key, value in dictionary.iteritems())
1659+
1660+
1661+def find(predicate, items):
1662+ for item in items:
1663+ if predicate(item) is True:
1664+ return item
1665+
1666+
1667+def merge_dicts(*dictionaries):
1668+ merged_dict = dict()
1669+ for dictionary in dictionaries:
1670+ merged_dict = dict(merged_dict.items() + dictionary.items())
1671+ return merged_dict
1672+
1673+
1674+def guid():
1675+ return str(uuid.uuid4())
1676+
1677+
1678+def remove_nones(hash):
1679+ return dict((key, value)
1680+ for key, value in hash.iteritems() if value is not None)
1681+
1682+
1683+class cached_property(object):
1684+ """
1685+ Taken from : https://github.com/nshah/python-memoize
1686+ A decorator that converts a function into a lazy property. The
1687+ function wrapped is called the first time to retrieve the result
1688+ and than that calculated result is used the next time you access
1689+ the value::
1690+
1691+ class Foo(object):
1692+
1693+ @cached_property
1694+ def bar(self):
1695+ # calculate something important here
1696+ return 42
1697+
1698+ """
1699+
1700+ def __init__(self, func, name=None, doc=None):
1701+ self.func = func
1702+ self.__name__ = name or func.__name__
1703+ self.__doc__ = doc or func.__doc__
1704+
1705+ def __get__(self, obj, type=None):
1706+ if obj is None:
1707+ return self
1708+ value = self.func(obj)
1709+ setattr(obj, self.__name__, value)
1710+ return value
1711+
1712+
1713+class MethodInspector(object):
1714+ def __init__(self, func):
1715+ self._func = func
1716+
1717+ @cached_property
1718+ def required_args(self):
1719+ return self.args[0:self.required_args_count]
1720+
1721+ @cached_property
1722+ def optional_args(self):
1723+ keys = self.args[self.required_args_count: len(self.args)]
1724+ return zip(keys, self.defaults)
1725+
1726+ @cached_property
1727+ def defaults(self):
1728+ return self.argspec.defaults or ()
1729+
1730+ @cached_property
1731+ def required_args_count(self):
1732+ return len(self.args) - len(self.defaults)
1733+
1734+ @cached_property
1735+ def args(self):
1736+ args = self.argspec.args
1737+ if inspect.ismethod(self._func):
1738+ args.pop(0)
1739+ return args
1740+
1741+ @cached_property
1742+ def argspec(self):
1743+ return inspect.getargspec(self._func)
1744+
1745+ def __str__(self):
1746+ optionals = ["%s=%s" % (k, v) for k, v in self.optional_args]
1747+ args_str = ' '.join(map(lambda arg: "<%s>" % arg,
1748+ self.required_args + optionals))
1749+ return "%s %s" % (self._func.__name__, args_str)
1750
1751=== added file 'melange/common/wsgi.py'
1752--- melange/common/wsgi.py 1970-01-01 00:00:00 +0000
1753+++ melange/common/wsgi.py 2011-08-25 11:33:54 +0000
1754@@ -0,0 +1,383 @@
1755+# vim: tabstop=4 shiftwidth=4 softtabstop=4
1756+
1757+# Copyright 2010 OpenStack LLC.
1758+# All Rights Reserved.
1759+#
1760+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1761+# not use this file except in compliance with the License. You may obtain
1762+# a copy of the License at
1763+#
1764+# http://www.apache.org/licenses/LICENSE-2.0
1765+#
1766+# Unless required by applicable law or agreed to in writing, software
1767+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1768+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1769+# License for the specific language governing permissions and limitations
1770+# under the License.
1771+
1772+"""
1773+Utility methods for working with WSGI servers
1774+"""
1775+import datetime
1776+from datetime import timedelta
1777+import eventlet.wsgi
1778+import inspect
1779+import json
1780+import logging
1781+import paste.urlmap
1782+import re
1783+import traceback
1784+from webob import Response
1785+import webob.dec
1786+import webob.exc
1787+from webob.exc import HTTPBadRequest
1788+from webob.exc import HTTPError
1789+from webob.exc import HTTPInternalServerError
1790+from webob.exc import HTTPNotAcceptable
1791+from webob.exc import HTTPNotFound
1792+from xml.dom import minidom
1793+
1794+from openstack.common.wsgi import Router, Server, Middleware
1795+
1796+from melange.common.exception import InvalidContentType
1797+from melange.common.exception import MelangeError
1798+from melange.common.utils import cached_property
1799+
1800+
1801+eventlet.patcher.monkey_patch(all=False, socket=True)
1802+
1803+LOG = logging.getLogger('melange.wsgi')
1804+
1805+
1806+def versioned_urlmap(*args, **kwargs):
1807+ urlmap = paste.urlmap.urlmap_factory(*args, **kwargs)
1808+ return VersionedURLMap(urlmap)
1809+
1810+
1811+class VersionedURLMap(object):
1812+
1813+ def __init__(self, urlmap):
1814+ self.urlmap = urlmap
1815+
1816+ def __call__(self, environ, start_response):
1817+ req = Request(environ)
1818+
1819+ if req.url_version is None and req.accept_version is not None:
1820+ version = "/v" + req.accept_version
1821+ app = self.urlmap.get(
1822+ version, Fault(HTTPNotAcceptable(_("version not supported"))))
1823+ else:
1824+ app = self.urlmap
1825+
1826+ return app(environ, start_response)
1827+
1828+
1829+class Request(webob.Request):
1830+
1831+ @property
1832+ def deserialized_params(self):
1833+ return Serializer().deserialize(self.body, self.get_content_type())
1834+
1835+ def best_match_content_type(self):
1836+ """Determine the most acceptable content-type.
1837+
1838+ Based on the query extension then the Accept header.
1839+
1840+ """
1841+ parts = self.path.rsplit('.', 1)
1842+
1843+ if len(parts) > 1:
1844+ format = parts[1]
1845+ if format in ['json', 'xml']:
1846+ return 'application/{0}'.format(parts[1])
1847+
1848+ ctypes = {'application/vnd.openstack.melange+json': "application/json",
1849+ 'application/vnd.openstack.melange+xml': "application/xml",
1850+ 'application/json': "application/json",
1851+ 'application/xml': "application/xml"}
1852+
1853+ bm = self.accept.best_match(ctypes.keys())
1854+ return ctypes.get(bm, 'application/json')
1855+
1856+ def get_content_type(self):
1857+ allowed_types = ("application/xml", "application/json")
1858+ self.content_type = self.content_type or "application/json"
1859+ type = self.content_type
1860+ if type in allowed_types:
1861+ return type
1862+ LOG.debug("Wrong Content-Type: %s" % type)
1863+ raise webob.exc.HTTPUnsupportedMediaType(
1864+ _("Content type %s not supported") % type)
1865+
1866+ @cached_property
1867+ def accept_version(self):
1868+ accept_header = self.headers.get('ACCEPT', "")
1869+ accept_version_re = re.compile(".*?application/vnd.openstack.melange"
1870+ "(\+.+?)?;"
1871+ "version=(?P<version_no>\d+\.?\d*)")
1872+
1873+ match = accept_version_re.search(accept_header)
1874+ return match.group("version_no") if match else None
1875+
1876+ @cached_property
1877+ def url_version(self):
1878+ versioned_url_re = re.compile("/v(?P<version_no>\d+\.?\d*)")
1879+ match = versioned_url_re.search(self.path)
1880+ return match.group("version_no") if match else None
1881+
1882+
1883+class Result(object):
1884+
1885+ def __init__(self, data, status=200):
1886+ self.data = data
1887+ self.status = status
1888+
1889+ def response(self, serializer, serialization_type):
1890+ serialized_data = self.serialize_data(serializer, serialization_type)
1891+ return Response(body=serialized_data, content_type=serialization_type,
1892+ status=self.status)
1893+
1894+ def serialize_data(self, serializer, serialization_type):
1895+ return serializer.serialize(self.data, serialization_type)
1896+
1897+
1898+class Controller(object):
1899+ """
1900+ WSGI app that reads routing information supplied by RoutesMiddleware
1901+ and calls the requested action method upon itself. All action methods
1902+ must, in addition to their normal parameters, accept a 'req' argument
1903+ which is the incoming webob.Request. They raise a webob.exc exception,
1904+ or return a dict which will be serialized by requested content type.
1905+ """
1906+ exception_map = {}
1907+ admin_actions = []
1908+
1909+ def __init__(self, admin_actions=None):
1910+ admin_actions = admin_actions or []
1911+ self.model_exception_map = self._invert_dict_list(self.exception_map)
1912+ self.admin_actions = admin_actions
1913+
1914+ @webob.dec.wsgify(RequestClass=Request)
1915+ def __call__(self, req):
1916+ """
1917+ Call the method specified in req.environ by RoutesMiddleware.
1918+ """
1919+ arg_dict = req.environ['wsgiorg.routing_args'][1]
1920+ action = arg_dict['action']
1921+ method = getattr(self, action, None)
1922+ del arg_dict['controller']
1923+ del arg_dict['action']
1924+ arg_dict['request'] = req
1925+
1926+ result = self._execute_action(method, arg_dict)
1927+
1928+ if type(result) is dict:
1929+ result = Result(result)
1930+
1931+ if isinstance(result, Result):
1932+ return result.response(self._serializer(),
1933+ req.best_match_content_type())
1934+ return result
1935+
1936+ def _execute_action(self, method, arg_dict):
1937+ if method is None:
1938+ raise HTTPNotFound
1939+ try:
1940+ if self._method_doesnt_expect_format_arg(method):
1941+ arg_dict.pop('format', None)
1942+ return method(**arg_dict)
1943+ except MelangeError as e:
1944+ LOG.debug(traceback.format_exc())
1945+ httpError = self._get_http_error(e)
1946+ return Fault(httpError(str(e), request=arg_dict['request']))
1947+ except HTTPError as e:
1948+ LOG.debug(traceback.format_exc())
1949+ return Fault(e)
1950+ except Exception as e:
1951+ LOG.exception(e)
1952+ return Fault(HTTPInternalServerError(e.message,
1953+ request=arg_dict['request']))
1954+
1955+ def _method_doesnt_expect_format_arg(self, method):
1956+ return not 'format' in inspect.getargspec(method)[0]
1957+
1958+ def _get_http_error(self, error):
1959+ return self.model_exception_map.get(type(error), HTTPBadRequest)
1960+
1961+ def _serializer(self):
1962+ """
1963+ Serialize the given dict to the response type requested in request.
1964+ Uses self._serialization_metadata if it exists, which is a dict mapping
1965+ MIME types to information needed to serialize to that type.
1966+ """
1967+ _metadata = getattr(type(self), "_serialization_metadata", {})
1968+ return Serializer(_metadata)
1969+
1970+ def _deserialize(self, data, content_type):
1971+ """Deserialize the request body to the specefied content type.
1972+
1973+ Uses self._serialization_metadata if it exists, which is a dict mapping
1974+ MIME types to information needed to serialize to that type.
1975+
1976+ """
1977+ _metadata = getattr(type(self), '_serialization_metadata', {})
1978+ serializer = Serializer(_metadata)
1979+ return serializer.deserialize(data, content_type)
1980+
1981+ def _invert_dict_list(self, exception_dict):
1982+ """
1983+ {'x':[1,2,3],'y':[4,5,6]} converted to
1984+ {1:'x',2:'x',3:'x',4:'y',5:'y',6:'y'}
1985+ """
1986+ inverted_dict = {}
1987+ for key, value_list in exception_dict.items():
1988+ for value in value_list:
1989+ inverted_dict[value] = key
1990+ return inverted_dict
1991+
1992+
1993+class Serializer(object):
1994+ """
1995+ Serializes a dictionary to a Content Type specified by a WSGI environment.
1996+ """
1997+
1998+ def __init__(self, metadata=None):
1999+ """
2000+ Create a serializer based on the given WSGI environment.
2001+ 'metadata' is an optional dict mapping MIME types to information
2002+ needed to serialize a dictionary to that type.
2003+ """
2004+ self.metadata = metadata or {}
2005+ self._methods = {
2006+ 'application/json': self._to_json,
2007+ 'application/xml': self._to_xml}
2008+
2009+ def serialize(self, data, content_type):
2010+ """
2011+ Serialize a dictionary into a string. The format of the string
2012+ will be decided based on the Content Type requested in self.environ:
2013+ by Accept: header, or by URL suffix.
2014+ """
2015+ return self._methods.get(content_type, repr)(data)
2016+
2017+ def _to_json(self, data):
2018+ def sanitizer(obj):
2019+ if isinstance(obj, datetime.datetime):
2020+ _dtime = obj - timedelta(microseconds=obj.microsecond)
2021+ return _dtime.isoformat()
2022+ return obj
2023+
2024+ return json.dumps(data, default=sanitizer)
2025+
2026+ def _to_xml(self, data):
2027+ metadata = self.metadata.get('application/xml', {})
2028+ # We expect data to contain a single key which is the XML root.
2029+ root_key = data.keys()[0]
2030+ doc = minidom.Document()
2031+ node = self._to_xml_node(doc, metadata, root_key, data[root_key])
2032+ return node.toprettyxml(indent=' ')
2033+
2034+ def _to_xml_node(self, doc, metadata, nodename, data):
2035+ """Recursive method to convert data members to XML nodes."""
2036+ if hasattr(data, 'to_xml'):
2037+ return data.to_xml()
2038+ result = doc.createElement(nodename)
2039+ if type(data) is list:
2040+ singular = metadata.get('plurals', {}).get(nodename, None)
2041+ if singular is None:
2042+ if nodename.endswith('s'):
2043+ singular = nodename[:-1]
2044+ else:
2045+ singular = 'item'
2046+ for item in data:
2047+ node = self._to_xml_node(doc, metadata, singular, item)
2048+ result.appendChild(node)
2049+ elif type(data) is dict:
2050+ attrs = metadata.get('attributes', {}).get(nodename, {})
2051+ for k, v in data.items():
2052+ if k in attrs:
2053+ result.setAttribute(k, str(v))
2054+ else:
2055+ node = self._to_xml_node(doc, metadata, k, v)
2056+ result.appendChild(node)
2057+ else: # atom
2058+ node = doc.createTextNode(str(data))
2059+ result.appendChild(node)
2060+ return result
2061+
2062+ def deserialize(self, datastring, content_type):
2063+ """Deserialize a string to a dictionary.
2064+
2065+ The string must be in the format of a supported MIME type.
2066+
2067+ """
2068+ return self.get_deserialize_handler(content_type)(datastring)
2069+
2070+ def get_deserialize_handler(self, content_type):
2071+ handlers = {
2072+ 'application/json': self._from_json,
2073+ 'application/xml': self._from_xml,
2074+ }
2075+
2076+ try:
2077+ return handlers[content_type]
2078+ except Exception:
2079+ raise InvalidContentType(content_type=content_type)
2080+
2081+ def _from_json(self, datastring):
2082+ return json.loads(datastring or "{}")
2083+
2084+ def _from_xml(self, datastring):
2085+ xmldata = self.metadata.get('application/xml', {})
2086+ plurals = set(xmldata.get('plurals', {}))
2087+ node = minidom.parseString(datastring).childNodes[0]
2088+ return {node.nodeName: self._from_xml_node(node, plurals)}
2089+
2090+ def _from_xml_node(self, node, listnames):
2091+ """Convert a minidom node to a simple Python type.
2092+
2093+ listnames is a collection of names of XML nodes whose subnodes should
2094+ be considered list items.
2095+
2096+ """
2097+ if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3:
2098+ return node.childNodes[0].nodeValue
2099+ elif node.nodeName in listnames:
2100+ return [self._from_xml_node(n, listnames) for n in node.childNodes]
2101+ else:
2102+ result = dict()
2103+ for attr in node.attributes.keys():
2104+ result[attr] = node.attributes[attr].nodeValue
2105+ for child in node.childNodes:
2106+ if child.nodeType != node.TEXT_NODE:
2107+ result[child.nodeName] = self._from_xml_node(child,
2108+ listnames)
2109+ return result
2110+
2111+
2112+class Fault(webob.exc.HTTPException):
2113+ """Error codes for API faults"""
2114+
2115+ def __init__(self, exception):
2116+ """Create a Fault for the given webob.exc.exception."""
2117+ self.wrapped_exc = exception
2118+
2119+ @webob.dec.wsgify(RequestClass=Request)
2120+ def __call__(self, req):
2121+ """Generate a WSGI response based on the exception passed to ctor."""
2122+ # Replace the body with fault details.
2123+ fault_name = self.wrapped_exc.__class__.__name__
2124+ if(fault_name.startswith("HTTP")):
2125+ fault_name = fault_name[4:]
2126+ fault_data = {
2127+ fault_name: {
2128+ 'code': self.wrapped_exc.status_int,
2129+ 'message': self.wrapped_exc.explanation,
2130+ 'detail': self.wrapped_exc.detail}}
2131+ # 'code' is an attribute on the fault tag itself
2132+ metadata = {'application/xml': {'attributes': {fault_name: 'code'}}}
2133+ serializer = Serializer(metadata)
2134+ content_type = req.best_match_content_type()
2135+ self.wrapped_exc.body = serializer.serialize(fault_data, content_type)
2136+ self.wrapped_exc.content_type = content_type
2137+ return self.wrapped_exc
2138
2139=== added directory 'melange/db'
2140=== added file 'melange/db/__init__.py'
2141--- melange/db/__init__.py 1970-01-01 00:00:00 +0000
2142+++ melange/db/__init__.py 2011-08-25 11:33:54 +0000
2143@@ -0,0 +1,43 @@
2144+# vim: tabstop=4 shiftwidth=4 softtabstop=4
2145+
2146+# Copyright 2010-2011 OpenStack LLC.
2147+# All Rights Reserved.
2148+#
2149+# Licensed under the Apache License, Version 2.0 (the "License"); you may
2150+# not use this file except in compliance with the License. You may obtain
2151+# a copy of the License at
2152+#
2153+# http://www.apache.org/licenses/LICENSE-2.0
2154+#
2155+# Unless required by applicable law or agreed to in writing, software
2156+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
2157+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
2158+# License for the specific language governing permissions and limitations
2159+# under the License.
2160+import optparse
2161+
2162+from melange.common import utils
2163+from melange.common.config import Config
2164+
2165+
2166+db_api = utils.import_object(Config.get("db_api_implementation",
2167+ "melange.db.sqlalchemy.api"))
2168+
2169+
2170+def add_options(parser):
2171+ """
2172+ Adds any configuration options that the db layer might have.
2173+
2174+ :param parser: An optparse.OptionParser object
2175+ :retval None
2176+ """
2177+ help_text = "The following configuration options are specific to the "\
2178+ "Melange image registry database."
2179+
2180+ group = optparse.OptionGroup(parser, "Registry Database Options",
2181+ help_text)
2182+ group.add_option('--sql-connection', metavar="CONNECTION",
2183+ default=None,
2184+ help="A valid SQLAlchemy connection string for the "
2185+ "registry database. Default: %default")
2186+ parser.add_option_group(group)
2187
2188=== added directory 'melange/db/sqlalchemy'
2189=== added file 'melange/db/sqlalchemy/__init__.py'
2190--- melange/db/sqlalchemy/__init__.py 1970-01-01 00:00:00 +0000
2191+++ melange/db/sqlalchemy/__init__.py 2011-08-25 11:33:54 +0000
2192@@ -0,0 +1,16 @@
2193+# vim: tabstop=4 shiftwidth=4 softtabstop=4
2194+
2195+# Copyright 2010-2011 OpenStack LLC.
2196+# All Rights Reserved.
2197+#
2198+# Licensed under the Apache License, Version 2.0 (the "License"); you may
2199+# not use this file except in compliance with the License. You may obtain
2200+# a copy of the License at
2201+#
2202+# http://www.apache.org/licenses/LICENSE-2.0
2203+#
2204+# Unless required by applicable law or agreed to in writing, software
2205+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
2206+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
2207+# License for the specific language governing permissions and limitations
2208+# under the License.
2209
2210=== added file 'melange/db/sqlalchemy/api.py'
2211--- melange/db/sqlalchemy/api.py 1970-01-01 00:00:00 +0000
2212+++ melange/db/sqlalchemy/api.py 2011-08-25 11:33:54 +0000
2213@@ -0,0 +1,213 @@
2214+# vim: tabstop=4 shiftwidth=4 softtabstop=4
2215+
2216+# Copyright 2011 OpenStack LLC.
2217+# All Rights Reserved.
2218+#
2219+# Licensed under the Apache License, Version 2.0 (the "License"); you may
2220+# not use this file except in compliance with the License. You may obtain
2221+# a copy of the License at
2222+#
2223+# http://www.apache.org/licenses/LICENSE-2.0
2224+#
2225+# Unless required by applicable law or agreed to in writing, software
2226+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
2227+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
2228+# License for the specific language governing permissions and limitations
2229+# under the License.
2230+from sqlalchemy import and_
2231+from sqlalchemy import or_
2232+from sqlalchemy.orm import aliased
2233+
2234+from melange import ipam
2235+from melange.common import utils
2236+from melange.db.sqlalchemy import migration
2237+from melange.db.sqlalchemy import session
2238+from melange.db.sqlalchemy.mappers import IpNat
2239+
2240+
2241+def find_all_by(model, **conditions):
2242+ return _query_by(model, **conditions).all()
2243+
2244+
2245+def find_all_by_limit(model, conditions, limit, marker=None,
2246+ marker_column=None):
2247+ return _limits(model, conditions, limit, marker, marker_column).all()
2248+
2249+
2250+def find_by(model, **kwargs):
2251+ return _query_by(model, **kwargs).first()
2252+
2253+
2254+def save(model):
2255+ db_session = session.get_session()
2256+ model = db_session.merge(model)
2257+ db_session.flush()
2258+ return model
2259+
2260+
2261+def delete(model):
2262+ model.deleted = True
2263+ save(model)
2264+
2265+
2266+def delete_all(model, **conditions):
2267+ _delete_all(_query_by(model, **conditions))
2268+
2269+
2270+def update(model, values):
2271+ for k, v in values.iteritems():
2272+ model[k] = v
2273+
2274+
2275+def update_all(model, conditions, values):
2276+ _query_by(model, **conditions).update(values)
2277+
2278+
2279+def find_inside_globals_for(local_address_id, **kwargs):
2280+ marker_column = IpNat.inside_global_address_id
2281+ limit = kwargs.pop('limit', 200)
2282+ marker = kwargs.pop('marker', None)
2283+
2284+ kwargs["inside_local_address_id"] = local_address_id
2285+ query = _limits(IpNat, kwargs,
2286+ limit, marker, marker_column)
2287+ return [nat.inside_global_address for nat in query]
2288+
2289+
2290+def find_inside_locals_for(global_address_id, **kwargs):
2291+ marker_column = IpNat.inside_local_address_id
2292+ limit = kwargs.pop('limit', 200)
2293+ marker = kwargs.pop('marker', None)
2294+
2295+ kwargs["inside_global_address_id"] = global_address_id
2296+ query = _limits(IpNat, kwargs,
2297+ limit, marker, marker_column)
2298+ return [nat.inside_local_address for nat in query]
2299+
2300+
2301+def save_nat_relationships(nat_relationships):
2302+ ip_nat_table = IpNat
2303+ for relationship in nat_relationships:
2304+ ip_nat = ip_nat_table()
2305+ relationship['id'] = utils.guid()
2306+ update(ip_nat, relationship)
2307+ save(ip_nat)
2308+
2309+
2310+def remove_inside_globals(local_address_id,
2311+ inside_global_address=None):
2312+
2313+ def _filter_inside_global_address(natted_ips, inside_global_address):
2314+ return natted_ips.\
2315+ join((ipam.models.IpAddress,
2316+ IpNat.inside_global_address_id == ipam.models.IpAddress.id)).\
2317+ filter(ipam.models.IpAddress.address == inside_global_address)
2318+
2319+ remove_natted_ips(_filter_inside_global_address,
2320+ inside_global_address,
2321+ inside_local_address_id=local_address_id)
2322+
2323+
2324+def remove_inside_locals(global_address_id,
2325+ inside_local_address=None):
2326+
2327+ def _filter_inside_local_address(natted_ips, inside_local_address):
2328+ return natted_ips.\
2329+ join((ipam.models.IpAddress,
2330+ IpNat.inside_local_address_id == ipam.models.IpAddress.id)).\
2331+ filter(ipam.models.IpAddress.address == inside_local_address)
2332+
2333+ remove_natted_ips(_filter_inside_local_address,
2334+ inside_local_address,
2335+ inside_global_address_id=global_address_id)
2336+
2337+
2338+def remove_natted_ips(_filter_by_natted_address,
2339+ natted_address, **kwargs):
2340+ natted_ips = find_natted_ips(**kwargs)
2341+ if natted_address != None:
2342+ natted_ips = _filter_by_natted_address(natted_ips, natted_address)
2343+ for ip in natted_ips:
2344+ delete(ip)
2345+
2346+
2347+def find_natted_ips(**kwargs):
2348+ return _base_query(IpNat).\
2349+ filter_by(**kwargs)
2350+
2351+
2352+def find_all_blocks_with_deallocated_ips():
2353+ return _base_query(ipam.models.IpBlock).\
2354+ join(ipam.models.IpAddress).\
2355+ filter(ipam.models.IpAddress.marked_for_deallocation == True)
2356+
2357+
2358+def delete_deallocated_ips(deallocated_by, **kwargs):
2359+ return _delete_all(_query_by(ipam.models.IpAddress, **kwargs).\
2360+ filter_by(marked_for_deallocation=True).\
2361+ filter(ipam.models.IpAddress.deallocated_at <= deallocated_by))
2362+
2363+
2364+def find_all_top_level_blocks_in_network(network_id):
2365+ parent_block = aliased(ipam.models.IpBlock, name="parent_block")
2366+
2367+ return _base_query(ipam.models.IpBlock).\
2368+ outerjoin((parent_block,
2369+ and_(ipam.models.IpBlock.parent_id == parent_block.id,
2370+ parent_block.network_id == network_id))).\
2371+ filter(ipam.models.IpBlock.network_id == network_id).\
2372+ filter(parent_block.id == None)
2373+
2374+
2375+def find_all_ips_in_network(network_id, **conditions):
2376+ return _query_by(ipam.models.IpAddress, **conditions).\
2377+ join(ipam.models.IpBlock).\
2378+ filter(ipam.models.IpBlock.network_id == network_id)
2379+
2380+
2381+def configure_db(options):
2382+ session.configure_db(options)
2383+
2384+
2385+def drop_db(options):
2386+ session.drop_db(options)
2387+
2388+
2389+def clean_db():
2390+ session.clean_db()
2391+
2392+
2393+def db_sync(options, version=None):
2394+ migration.db_sync(options, version)
2395+
2396+
2397+def db_upgrade(options, version=None):
2398+ migration.upgrade(options, version)
2399+
2400+
2401+def db_downgrade(options, version):
2402+ migration.downgrade(options, version)
2403+
2404+
2405+def _delete_all(query):
2406+ query.update({'deleted': True})
2407+
2408+
2409+def _base_query(cls):
2410+ return session.get_session().query(cls).\
2411+ filter(or_(cls.deleted == False, cls.deleted == None))
2412+
2413+
2414+def _query_by(cls, **conditions):
2415+ query = _base_query(cls)
2416+ if conditions:
2417+ query = query.filter_by(**conditions)
2418+ return query
2419+
2420+
2421+def _limits(cls, conditions, limit, marker, marker_column=None):
2422+ query = _query_by(cls, **conditions)
2423+ marker_column = marker_column or cls.id
2424+ if (marker is not None):
2425+ query = query.filter(marker_column > marker)
2426+ return query.order_by(marker_column).limit(limit)
2427
2428=== added file 'melange/db/sqlalchemy/mappers.py'
2429--- melange/db/sqlalchemy/mappers.py 1970-01-01 00:00:00 +0000
2430+++ melange/db/sqlalchemy/mappers.py 2011-08-25 11:33:54 +0000
2431@@ -0,0 +1,53 @@
2432+# vim: tabstop=4 shiftwidth=4 softtabstop=4
2433+
2434+# Copyright 2011 OpenStack LLC.
2435+# All Rights Reserved.
2436+#
2437+# Licensed under the Apache License, Version 2.0 (the "License"); you may
2438+# not use this file except in compliance with the License. You may obtain
2439+# a copy of the License at
2440+#
2441+# http://www.apache.org/licenses/LICENSE-2.0
2442+#
2443+# Unless required by applicable law or agreed to in writing, software
2444+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
2445+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
2446+# License for the specific language governing permissions and limitations
2447+# under the License.
2448+from sqlalchemy import MetaData
2449+from sqlalchemy import Table
2450+from sqlalchemy.orm import mapper
2451+from sqlalchemy.orm import relation
2452+
2453+
2454+def map(engine, models):
2455+ meta = MetaData()
2456+ meta.bind = engine
2457+ ip_nats_table = Table('ip_nats', meta, autoload=True)
2458+ ip_addresses_table = Table('ip_addresses', meta, autoload=True)
2459+ policies_table = Table('policies', meta, autoload=True)
2460+ ip_ranges_table = Table('ip_ranges', meta, autoload=True)
2461+ ip_octets_table = Table('ip_octets', meta, autoload=True)
2462+
2463+ mapper(models["IpBlock"], Table('ip_blocks', meta, autoload=True))
2464+ mapper(models["IpAddress"], ip_addresses_table)
2465+ mapper(models["Policy"], policies_table)
2466+ mapper(models["IpRange"], ip_ranges_table)
2467+ mapper(models["IpOctet"], ip_octets_table)
2468+ mapper(IpNat, ip_nats_table,
2469+ properties={'inside_global_address':
2470+ relation(models["IpAddress"],
2471+ primaryjoin=ip_nats_table.c.inside_global_address_id \
2472+ == ip_addresses_table.c.id),
2473+ 'inside_local_address': relation(models["IpAddress"],
2474+ primaryjoin=ip_nats_table.c.\
2475+ inside_local_address_id == \
2476+ ip_addresses_table.c.id)})
2477+
2478+
2479+class IpNat(object):
2480+ def __setitem__(self, key, value):
2481+ setattr(self, key, value)
2482+
2483+ def __getitem__(self, key):
2484+ return getattr(self, key)
2485
2486=== added directory 'melange/db/sqlalchemy/migrate_repo'
2487=== added file 'melange/db/sqlalchemy/migrate_repo/README'
2488--- melange/db/sqlalchemy/migrate_repo/README 1970-01-01 00:00:00 +0000
2489+++ melange/db/sqlalchemy/migrate_repo/README 2011-08-25 11:33:54 +0000
2490@@ -0,0 +1,4 @@
2491+This is a database migration repository.
2492+
2493+More information at
2494+http://code.google.com/p/sqlalchemy-migrate/
2495
2496=== added file 'melange/db/sqlalchemy/migrate_repo/__init__.py'
2497--- melange/db/sqlalchemy/migrate_repo/__init__.py 1970-01-01 00:00:00 +0000
2498+++ melange/db/sqlalchemy/migrate_repo/__init__.py 2011-08-25 11:33:54 +0000
2499@@ -0,0 +1,18 @@
2500+# vim: tabstop=4 shiftwidth=4 softtabstop=4
2501+
2502+# Copyright 2011 OpenStack LLC.
2503+# All Rights Reserved.
2504+#
2505+# Licensed under the Apache License, Version 2.0 (the "License"); you may
2506+# not use this file except in compliance with the License. You may obtain
2507+# a copy of the License at
2508+#
2509+# http://www.apache.org/licenses/LICENSE-2.0
2510+#
2511+# Unless required by applicable law or agreed to in writing, software
2512+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
2513+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
2514+# License for the specific language governing permissions and limitations
2515+# under the License.
2516+
2517+# template repository default module
2518
2519=== added file 'melange/db/sqlalchemy/migrate_repo/manage.py'
2520--- melange/db/sqlalchemy/migrate_repo/manage.py 1970-01-01 00:00:00 +0000
2521+++ melange/db/sqlalchemy/migrate_repo/manage.py 2011-08-25 11:33:54 +0000
2522@@ -0,0 +1,21 @@
2523+#!/usr/bin/env python
2524+# vim: tabstop=4 shiftwidth=4 softtabstop=4
2525+
2526+# Copyright 2011 OpenStack LLC.
2527+# All Rights Reserved.
2528+#
2529+# Licensed under the Apache License, Version 2.0 (the "License"); you may
2530+# not use this file except in compliance with the License. You may obtain
2531+# a copy of the License at
2532+#
2533+# http://www.apache.org/licenses/LICENSE-2.0
2534+#
2535+# Unless required by applicable law or agreed to in writing, software
2536+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
2537+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
2538+# License for the specific language governing permissions and limitations
2539+# under the License.
2540+from migrate.versioning.shell import main
2541+
2542+
2543+main(debug='False', repository='.')
2544
2545=== added file 'melange/db/sqlalchemy/migrate_repo/migrate.cfg'
2546--- melange/db/sqlalchemy/migrate_repo/migrate.cfg 1970-01-01 00:00:00 +0000
2547+++ melange/db/sqlalchemy/migrate_repo/migrate.cfg 2011-08-25 11:33:54 +0000
2548@@ -0,0 +1,20 @@
2549+[db_settings]
2550+# Used to identify which repository this database is versioned under.
2551+# You can use the name of your project.
2552+repository_id=Melange Migrations
2553+
2554+# The name of the database table used to track the schema version.
2555+# This name shouldn't already be used by your project.
2556+# If this is changed once a database is under version control, you'll need to
2557+# change the table name in each database too.
2558+version_table=migrate_version
2559+
2560+# When committing a change script, Migrate will attempt to generate the
2561+# sql for all supported databases; normally, if one of them fails - probably
2562+# because you don't have that database installed - it is ignored and the
2563+# commit continues, perhaps ending successfully.
2564+# Databases in this list MUST compile successfully during a commit, or the
2565+# entire commit will fail. List the databases your application will actually
2566+# be using to ensure your updates to that database work properly.
2567+# This must be a list; example: ['postgres','sqlite']
2568+required_dbs=['mysql','postgres','sqlite']
2569
2570=== added file 'melange/db/sqlalchemy/migrate_repo/schema.py'
2571--- melange/db/sqlalchemy/migrate_repo/schema.py 1970-01-01 00:00:00 +0000
2572+++ melange/db/sqlalchemy/migrate_repo/schema.py 2011-08-25 11:33:54 +0000
2573@@ -0,0 +1,65 @@
2574+# vim: tabstop=4 shiftwidth=4 softtabstop=4
2575+
2576+# Copyright 2011 OpenStack LLC.
2577+# All Rights Reserved.
2578+#
2579+# Licensed under the Apache License, Version 2.0 (the "License"); you may
2580+# not use this file except in compliance with the License. You may obtain
2581+# a copy of the License at
2582+#
2583+# http://www.apache.org/licenses/LICENSE-2.0
2584+#
2585+# Unless required by applicable law or agreed to in writing, software
2586+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
2587+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
2588+# License for the specific language governing permissions and limitations
2589+# under the License.
2590+
2591+"""
2592+Various conveniences used for migration scripts
2593+"""
2594+
2595+import logging
2596+import sqlalchemy.types
2597+
2598+
2599+logger = logging.getLogger('melange.db.migrate_repo.schema')
2600+
2601+
2602+String = lambda length: sqlalchemy.types.String(
2603+ length=length, convert_unicode=False, assert_unicode=None,
2604+ unicode_error=None, _warn_on_bytestring=False)
2605+
2606+
2607+Text = lambda: sqlalchemy.types.Text(
2608+ length=None, convert_unicode=False, assert_unicode=None,
2609+ unicode_error=None, _warn_on_bytestring=False)
2610+
2611+
2612+Boolean = lambda: sqlalchemy.types.Boolean(create_constraint=True, name=None)
2613+
2614+
2615+DateTime = lambda: sqlalchemy.types.DateTime(timezone=False)
2616+
2617+
2618+Integer = lambda: sqlalchemy.types.Integer()
2619+
2620+
2621+BigInteger = lambda: sqlalchemy.types.BigInteger()
2622+
2623+
2624+def create_tables(tables):
2625+ for table in tables:
2626+ logger.info("creating table %(table)s" % locals())
2627+ table.create()
2628+
2629+
2630+def drop_tables(tables):
2631+ for table in tables:
2632+ logger.info("dropping table %(table)s" % locals())
2633+ table.drop()
2634+
2635+
2636+def Table(name, metadata, *args, **kwargs):
2637+ return sqlalchemy.schema.Table(name, metadata, *args,
2638+ mysql_engine='INNODB', **kwargs)
2639
2640=== added directory 'melange/db/sqlalchemy/migrate_repo/versions'
2641=== added file 'melange/db/sqlalchemy/migrate_repo/versions/001_add_ip_blocks_table.py'
2642--- melange/db/sqlalchemy/migrate_repo/versions/001_add_ip_blocks_table.py 1970-01-01 00:00:00 +0000
2643+++ melange/db/sqlalchemy/migrate_repo/versions/001_add_ip_blocks_table.py 2011-08-25 11:33:54 +0000
2644@@ -0,0 +1,48 @@
2645+# vim: tabstop=4 shiftwidth=4 softtabstop=4
2646+
2647+# Copyright 2011 OpenStack LLC.
2648+# All Rights Reserved.
2649+#
2650+# Licensed under the Apache License, Version 2.0 (the "License"); you may
2651+# not use this file except in compliance with the License. You may obtain
2652+# a copy of the License at
2653+#
2654+# http://www.apache.org/licenses/LICENSE-2.0
2655+#
2656+# Unless required by applicable law or agreed to in writing, software
2657+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
2658+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
2659+# License for the specific language governing permissions and limitations
2660+# under the License.
2661+from sqlalchemy.schema import Column
2662+from sqlalchemy.schema import MetaData
2663+
2664+from melange.db.sqlalchemy.migrate_repo.schema import create_tables
2665+from melange.db.sqlalchemy.migrate_repo.schema import DateTime
2666+from melange.db.sqlalchemy.migrate_repo.schema import drop_tables
2667+from melange.db.sqlalchemy.migrate_repo.schema import String
2668+from melange.db.sqlalchemy.migrate_repo.schema import Table
2669+
2670+
2671+def define_ip_blocks_table(meta):
2672+ ip_blocks = Table('ip_blocks', meta,
2673+ Column('id', String(36), primary_key=True, nullable=False),
2674+ Column('network_id', String(255)),
2675+ Column('cidr', String(255), nullable=False),
2676+ Column('created_at', DateTime(), nullable=True),
2677+ Column('updated_at', DateTime()))
2678+ return ip_blocks
2679+
2680+
2681+def upgrade(migrate_engine):
2682+ meta = MetaData()
2683+ meta.bind = migrate_engine
2684+ tables = [define_ip_blocks_table(meta)]
2685+ create_tables(tables)
2686+
2687+
2688+def downgrade(migrate_engine):
2689+ meta = MetaData()
2690+ meta.bind = migrate_engine
2691+ tables = [define_ip_blocks_table(meta)]
2692+ drop_tables(tables)
2693
2694=== added file 'melange/db/sqlalchemy/migrate_repo/versions/002_add_ip_addresses_table.py'
2695--- melange/db/sqlalchemy/migrate_repo/versions/002_add_ip_addresses_table.py 1970-01-01 00:00:00 +0000
2696+++ melange/db/sqlalchemy/migrate_repo/versions/002_add_ip_addresses_table.py 2011-08-25 11:33:54 +0000
2697@@ -0,0 +1,54 @@
2698+# vim: tabstop=4 shiftwidth=4 softtabstop=4
2699+
2700+# Copyright 2011 OpenStack LLC.
2701+# All Rights Reserved.
2702+#
2703+# Licensed under the Apache License, Version 2.0 (the "License"); you may
2704+# not use this file except in compliance with the License. You may obtain
2705+# a copy of the License at
2706+#
2707+# http://www.apache.org/licenses/LICENSE-2.0
2708+#
2709+# Unless required by applicable law or agreed to in writing, software
2710+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
2711+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
2712+# License for the specific language governing permissions and limitations
2713+# under the License.
2714+from sqlalchemy.schema import Column
2715+from sqlalchemy.schema import ForeignKey
2716+from sqlalchemy.schema import MetaData
2717+
2718+from melange.db.sqlalchemy.migrate_repo.schema import create_tables
2719+from melange.db.sqlalchemy.migrate_repo.schema import DateTime
2720+from melange.db.sqlalchemy.migrate_repo.schema import drop_tables
2721+from melange.db.sqlalchemy.migrate_repo.schema import String
2722+from melange.db.sqlalchemy.migrate_repo.schema import Table
2723+
2724+
2725+def define_ip_addresses_table(meta):
2726+
2727+ ip_blocks = Table('ip_blocks', meta, autoload=True)
2728+
2729+ ip_addresses = Table('ip_addresses', meta,
2730+ Column('id', String(36), primary_key=True, nullable=False),
2731+ Column('address', String(255), nullable=False),
2732+ Column('interface_id', String(255), nullable=True),
2733+ Column('ip_block_id', String(36), ForeignKey('ip_blocks.id'),
2734+ nullable=True),
2735+ Column('created_at', DateTime(), nullable=True),
2736+ Column('updated_at', DateTime()))
2737+ return ip_addresses
2738+
2739+
2740+def upgrade(migrate_engine):
2741+ meta = MetaData()
2742+ meta.bind = migrate_engine
2743+ tables = [define_ip_addresses_table(meta)]
2744+ create_tables(tables)
2745+
2746+
2747+def downgrade(migrate_engine):
2748+ meta = MetaData()
2749+ meta.bind = migrate_engine
2750+ tables = [define_ip_addresses_table(meta)]
2751+ drop_tables(tables)
2752
2753=== added file 'melange/db/sqlalchemy/migrate_repo/versions/003_add_type_to_ip_blocks.py'
2754--- melange/db/sqlalchemy/migrate_repo/versions/003_add_type_to_ip_blocks.py 1970-01-01 00:00:00 +0000
2755+++ melange/db/sqlalchemy/migrate_repo/versions/003_add_type_to_ip_blocks.py 2011-08-25 11:33:54 +0000
2756@@ -0,0 +1,33 @@
2757+# vim: tabstop=4 shiftwidth=4 softtabstop=4
2758+
2759+# Copyright 2011 OpenStack LLC.
2760+# All Rights Reserved.
2761+#
2762+# Licensed under the Apache License, Version 2.0 (the "License"); you may
2763+# not use this file except in compliance with the License. You may obtain
2764+# a copy of the License at
2765+#
2766+# http://www.apache.org/licenses/LICENSE-2.0
2767+#
2768+# Unless required by applicable law or agreed to in writing, software
2769+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
2770+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
2771+# License for the specific language governing permissions and limitations
2772+# under the License.
2773+from sqlalchemy.schema import Column
2774+from sqlalchemy.schema import MetaData
2775+
2776+from melange.db.sqlalchemy.migrate_repo.schema import String
2777+from melange.db.sqlalchemy.migrate_repo.schema import Table
2778+
2779+
2780+def upgrade(migrate_engine):
2781+ meta = MetaData()
2782+ meta.bind = migrate_engine
2783+ Column('type', String(7)).create(Table('ip_blocks', meta))
2784+
2785+
2786+def downgrade(migrate_engine):
2787+ meta = MetaData()
2788+ meta.bind = migrate_engine
2789+ Table('ip_blocks', meta, autoload=True).columns["type"].drop()
2790
2791=== added file 'melange/db/sqlalchemy/migrate_repo/versions/004_add_ip_nat_table.py'
2792--- melange/db/sqlalchemy/migrate_repo/versions/004_add_ip_nat_table.py 1970-01-01 00:00:00 +0000
2793+++ melange/db/sqlalchemy/migrate_repo/versions/004_add_ip_nat_table.py 2011-08-25 11:33:54 +0000
2794@@ -0,0 +1,56 @@
2795+# vim: tabstop=4 shiftwidth=4 softtabstop=4
2796+
2797+# Copyright 2011 OpenStack LLC.
2798+# All Rights Reserved.
2799+#
2800+# Licensed under the Apache License, Version 2.0 (the "License"); you may
2801+# not use this file except in compliance with the License. You may obtain
2802+# a copy of the License at
2803+#
2804+# http://www.apache.org/licenses/LICENSE-2.0
2805+#
2806+# Unless required by applicable law or agreed to in writing, software
2807+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
2808+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
2809+# License for the specific language governing permissions and limitations
2810+# under the License.
2811+import datetime
2812+from sqlalchemy.schema import Column
2813+from sqlalchemy.schema import ForeignKey
2814+from sqlalchemy.schema import MetaData
2815+
2816+from melange.db.sqlalchemy.migrate_repo.schema import create_tables
2817+from melange.db.sqlalchemy.migrate_repo.schema import DateTime
2818+from melange.db.sqlalchemy.migrate_repo.schema import drop_tables
2819+from melange.db.sqlalchemy.migrate_repo.schema import String
2820+from melange.db.sqlalchemy.migrate_repo.schema import Table
2821+
2822+
2823+def define_ip_nat_table(meta):
2824+
2825+ ip_addresses = Table('ip_addresses', meta, autoload=True)
2826+
2827+ ip_nats = Table('ip_nats', meta,
2828+ Column('id', String(36), primary_key=True, nullable=False),
2829+ Column('inside_local_address_id', String(36),
2830+ ForeignKey('ip_addresses.id'), nullable=False),
2831+ Column('inside_global_address_id', String(36),
2832+ ForeignKey('ip_addresses.id'), nullable=False),
2833+ Column('created_at', DateTime(),
2834+ default=datetime.datetime.utcnow, nullable=True),
2835+ Column('updated_at', DateTime(), default=datetime.datetime.utcnow))
2836+ return ip_nats
2837+
2838+
2839+def upgrade(migrate_engine):
2840+ meta = MetaData()
2841+ meta.bind = migrate_engine
2842+ tables = [define_ip_nat_table(meta)]
2843+ create_tables(tables)
2844+
2845+
2846+def downgrade(migrate_engine):
2847+ meta = MetaData()
2848+ meta.bind = migrate_engine
2849+ tables = [define_ip_nat_table(meta)]
2850+ drop_tables(tables)
2851
2852=== added file 'melange/db/sqlalchemy/migrate_repo/versions/005_add_soft_delete_to_blocks_addresses_and_ip_nats.py'
2853--- melange/db/sqlalchemy/migrate_repo/versions/005_add_soft_delete_to_blocks_addresses_and_ip_nats.py 1970-01-01 00:00:00 +0000
2854+++ melange/db/sqlalchemy/migrate_repo/versions/005_add_soft_delete_to_blocks_addresses_and_ip_nats.py 2011-08-25 11:33:54 +0000
2855@@ -0,0 +1,37 @@
2856+# vim: tabstop=4 shiftwidth=4 softtabstop=4
2857+
2858+# Copyright 2011 OpenStack LLC.
2859+# All Rights Reserved.
2860+#
2861+# Licensed under the Apache License, Version 2.0 (the "License"); you may
2862+# not use this file except in compliance with the License. You may obtain
2863+# a copy of the License at
2864+#
2865+# http://www.apache.org/licenses/LICENSE-2.0
2866+#
2867+# Unless required by applicable law or agreed to in writing, software
2868+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
2869+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
2870+# License for the specific language governing permissions and limitations
2871+# under the License.
2872+from sqlalchemy.schema import Column
2873+from sqlalchemy.schema import MetaData
2874+from sqlalchemy.schema import Table
2875+
2876+from melange.db.sqlalchemy.migrate_repo.schema import Boolean
2877+
2878+
2879+def upgrade(migrate_engine):
2880+ meta = MetaData()
2881+ meta.bind = migrate_engine
2882+ Column('deleted', Boolean()).create(Table('ip_blocks', meta))
2883+ Column('deleted', Boolean()).create(Table('ip_addresses', meta))
2884+ Column('deleted', Boolean()).create(Table('ip_nats', meta))
2885+
2886+
2887+def downgrade(migrate_engine):
2888+ meta = MetaData()
2889+ meta.bind = migrate_engine
2890+ Table('ip_nats', meta, autoload=True).columns["deleted"].drop()
2891+ Table('ip_addresses', meta, autoload=True).columns["deleted"].drop()
2892+ Table('ip_blocks', meta, autoload=True).columns["deleted"].drop()
2893
2894=== added file 'melange/db/sqlalchemy/migrate_repo/versions/006_add_deallocated_to_ip_addresses.py'
2895--- melange/db/sqlalchemy/migrate_repo/versions/006_add_deallocated_to_ip_addresses.py 1970-01-01 00:00:00 +0000
2896+++ melange/db/sqlalchemy/migrate_repo/versions/006_add_deallocated_to_ip_addresses.py 2011-08-25 11:33:54 +0000
2897@@ -0,0 +1,35 @@
2898+# vim: tabstop=4 shiftwidth=4 softtabstop=4
2899+
2900+# Copyright 2011 OpenStack LLC.
2901+# All Rights Reserved.
2902+#
2903+# Licensed under the Apache License, Version 2.0 (the "License"); you may
2904+# not use this file except in compliance with the License. You may obtain
2905+# a copy of the License at
2906+#
2907+# http://www.apache.org/licenses/LICENSE-2.0
2908+#
2909+# Unless required by applicable law or agreed to in writing, software
2910+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
2911+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
2912+# License for the specific language governing permissions and limitations
2913+# under the License.
2914+from sqlalchemy.schema import Column
2915+from sqlalchemy.schema import MetaData
2916+from sqlalchemy.schema import Table
2917+
2918+from melange.db.sqlalchemy.migrate_repo.schema import Boolean
2919+
2920+
2921+def upgrade(migrate_engine):
2922+ meta = MetaData()
2923+ meta.bind = migrate_engine
2924+ Column('marked_for_deallocation', Boolean()).create(\
2925+ Table('ip_addresses', meta))
2926+
2927+
2928+def downgrade(migrate_engine):
2929+ meta = MetaData()
2930+ meta.bind = migrate_engine
2931+ Table('ip_addresses', meta,
2932+ autoload=True).columns["marked_for_deallocation"].drop()
2933
2934=== added file 'melange/db/sqlalchemy/migrate_repo/versions/007_add_policy_table.py'
2935--- melange/db/sqlalchemy/migrate_repo/versions/007_add_policy_table.py 1970-01-01 00:00:00 +0000
2936+++ melange/db/sqlalchemy/migrate_repo/versions/007_add_policy_table.py 2011-08-25 11:33:54 +0000
2937@@ -0,0 +1,51 @@
2938+# vim: tabstop=4 shiftwidth=4 softtabstop=4
2939+
2940+# Copyright 2011 OpenStack LLC.
2941+# All Rights Reserved.
2942+#
2943+# Licensed under the Apache License, Version 2.0 (the "License"); you may
2944+# not use this file except in compliance with the License. You may obtain
2945+# a copy of the License at
2946+#
2947+# http://www.apache.org/licenses/LICENSE-2.0
2948+#
2949+# Unless required by applicable law or agreed to in writing, software
2950+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
2951+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
2952+# License for the specific language governing permissions and limitations
2953+# under the License.
2954+from sqlalchemy.schema import Column
2955+from sqlalchemy.schema import MetaData
2956+
2957+from melange.db.sqlalchemy.migrate_repo.schema import Boolean
2958+from melange.db.sqlalchemy.migrate_repo.schema import create_tables
2959+from melange.db.sqlalchemy.migrate_repo.schema import DateTime
2960+from melange.db.sqlalchemy.migrate_repo.schema import drop_tables
2961+from melange.db.sqlalchemy.migrate_repo.schema import String
2962+from melange.db.sqlalchemy.migrate_repo.schema import Table
2963+
2964+
2965+def define_policy_table(meta):
2966+
2967+ policies = Table('policies', meta,
2968+ Column('id', String(36), primary_key=True, nullable=False),
2969+ Column('name', String(255), nullable=False),
2970+ Column('description', String(255), nullable=True),
2971+ Column('created_at', DateTime(), nullable=True),
2972+ Column('updated_at', DateTime()),
2973+ Column('deleted', Boolean(), default=False))
2974+ return policies
2975+
2976+
2977+def upgrade(migrate_engine):
2978+ meta = MetaData()
2979+ meta.bind = migrate_engine
2980+ tables = [define_policy_table(meta)]
2981+ create_tables(tables)
2982+
2983+
2984+def downgrade(migrate_engine):
2985+ meta = MetaData()
2986+ meta.bind = migrate_engine
2987+ tables = [define_policy_table(meta)]
2988+ drop_tables(tables)
2989
2990=== added file 'melange/db/sqlalchemy/migrate_repo/versions/008_add_ip_ranges_table.py'
2991--- melange/db/sqlalchemy/migrate_repo/versions/008_add_ip_ranges_table.py 1970-01-01 00:00:00 +0000
2992+++ melange/db/sqlalchemy/migrate_repo/versions/008_add_ip_ranges_table.py 2011-08-25 11:33:54 +0000
2993@@ -0,0 +1,55 @@
2994+# vim: tabstop=4 shiftwidth=4 softtabstop=4
2995+
2996+# Copyright 2011 OpenStack LLC.
2997+# All Rights Reserved.
2998+#
2999+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3000+# not use this file except in compliance with the License. You may obtain
3001+# a copy of the License at
3002+#
3003+# http://www.apache.org/licenses/LICENSE-2.0
3004+#
3005+# Unless required by applicable law or agreed to in writing, software
3006+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3007+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3008+# License for the specific language governing permissions and limitations
3009+# under the License.
3010+from sqlalchemy.schema import Column
3011+from sqlalchemy.schema import ForeignKey
3012+from sqlalchemy.schema import MetaData
3013+
3014+from melange.db.sqlalchemy.migrate_repo.schema import Boolean
3015+from melange.db.sqlalchemy.migrate_repo.schema import create_tables
3016+from melange.db.sqlalchemy.migrate_repo.schema import DateTime
3017+from melange.db.sqlalchemy.migrate_repo.schema import drop_tables
3018+from melange.db.sqlalchemy.migrate_repo.schema import Integer
3019+from melange.db.sqlalchemy.migrate_repo.schema import String
3020+from melange.db.sqlalchemy.migrate_repo.schema import Table
3021+
3022+
3023+def define_ip_range_table(meta):
3024+ policy_table = Table('policies', meta, autoload=True)
3025+
3026+ ip_ranges = Table('ip_ranges', meta,
3027+ Column('id', String(36), primary_key=True, nullable=False),
3028+ Column('offset', Integer(), nullable=False),
3029+ Column('length', Integer(), nullable=False),
3030+ Column('policy_id', String(36), ForeignKey('policies.id')),
3031+ Column('created_at', DateTime(), nullable=True),
3032+ Column('updated_at', DateTime()),
3033+ Column('deleted', Boolean(), default=False))
3034+ return ip_ranges
3035+
3036+
3037+def upgrade(migrate_engine):
3038+ meta = MetaData()
3039+ meta.bind = migrate_engine
3040+ tables = [define_ip_range_table(meta)]
3041+ create_tables(tables)
3042+
3043+
3044+def downgrade(migrate_engine):
3045+ meta = MetaData()
3046+ meta.bind = migrate_engine
3047+ tables = [define_ip_range_table(meta)]
3048+ drop_tables(tables)
3049
3050=== added file 'melange/db/sqlalchemy/migrate_repo/versions/009_add_policy_id_to_ip_blocks.py'
3051--- melange/db/sqlalchemy/migrate_repo/versions/009_add_policy_id_to_ip_blocks.py 1970-01-01 00:00:00 +0000
3052+++ melange/db/sqlalchemy/migrate_repo/versions/009_add_policy_id_to_ip_blocks.py 2011-08-25 11:33:54 +0000
3053@@ -0,0 +1,38 @@
3054+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3055+
3056+# Copyright 2011 OpenStack LLC.
3057+# All Rights Reserved.
3058+#
3059+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3060+# not use this file except in compliance with the License. You may obtain
3061+# a copy of the License at
3062+#
3063+# http://www.apache.org/licenses/LICENSE-2.0
3064+#
3065+# Unless required by applicable law or agreed to in writing, software
3066+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3067+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3068+# License for the specific language governing permissions and limitations
3069+# under the License.
3070+from sqlalchemy.schema import Column
3071+from sqlalchemy.schema import MetaData
3072+from sqlalchemy.schema import Table
3073+from sqlalchemy.schema import ForeignKeyConstraint
3074+
3075+from melange.db.sqlalchemy.migrate_repo.schema import String
3076+
3077+
3078+def upgrade(migrate_engine):
3079+ meta = MetaData()
3080+ meta.bind = migrate_engine
3081+
3082+ policy_table = Table('policies', meta, autoload=True)
3083+ ip_block_table = Table('ip_blocks', meta)
3084+ Column('policy_id', String(36), nullable=True).create(ip_block_table)
3085+ ForeignKeyConstraint([ip_block_table.c.policy_id], [policy_table.c.id])
3086+
3087+
3088+def downgrade(migrate_engine):
3089+ meta = MetaData()
3090+ meta.bind = migrate_engine
3091+ Table('ip_blocks', meta, autoload=True).columns["policy_id"].drop()
3092
3093=== added file 'melange/db/sqlalchemy/migrate_repo/versions/010_add_ip_octets_table.py'
3094--- melange/db/sqlalchemy/migrate_repo/versions/010_add_ip_octets_table.py 1970-01-01 00:00:00 +0000
3095+++ melange/db/sqlalchemy/migrate_repo/versions/010_add_ip_octets_table.py 2011-08-25 11:33:54 +0000
3096@@ -0,0 +1,54 @@
3097+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3098+
3099+# Copyright 2011 OpenStack LLC.
3100+# All Rights Reserved.
3101+#
3102+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3103+# not use this file except in compliance with the License. You may obtain
3104+# a copy of the License at
3105+#
3106+# http://www.apache.org/licenses/LICENSE-2.0
3107+#
3108+# Unless required by applicable law or agreed to in writing, software
3109+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3110+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3111+# License for the specific language governing permissions and limitations
3112+# under the License.
3113+from sqlalchemy.schema import Column
3114+from sqlalchemy.schema import ForeignKey
3115+from sqlalchemy.schema import MetaData
3116+
3117+from melange.db.sqlalchemy.migrate_repo.schema import Boolean
3118+from melange.db.sqlalchemy.migrate_repo.schema import create_tables
3119+from melange.db.sqlalchemy.migrate_repo.schema import DateTime
3120+from melange.db.sqlalchemy.migrate_repo.schema import drop_tables
3121+from melange.db.sqlalchemy.migrate_repo.schema import Integer
3122+from melange.db.sqlalchemy.migrate_repo.schema import String
3123+from melange.db.sqlalchemy.migrate_repo.schema import Table
3124+
3125+
3126+def define_ip_octets_table(meta):
3127+ policy_table = Table('policies', meta, autoload=True)
3128+
3129+ ip_octets = Table('ip_octets', meta,
3130+ Column('id', String(36), primary_key=True, nullable=False),
3131+ Column('octet', Integer(), nullable=False),
3132+ Column('policy_id', String(36), ForeignKey('policies.id')),
3133+ Column('created_at', DateTime(), nullable=True),
3134+ Column('updated_at', DateTime()),
3135+ Column('deleted', Boolean(), default=False))
3136+ return ip_octets
3137+
3138+
3139+def upgrade(migrate_engine):
3140+ meta = MetaData()
3141+ meta.bind = migrate_engine
3142+ tables = [define_ip_octets_table(meta)]
3143+ create_tables(tables)
3144+
3145+
3146+def downgrade(migrate_engine):
3147+ meta = MetaData()
3148+ meta.bind = migrate_engine
3149+ tables = [define_ip_octets_table(meta)]
3150+ drop_tables(tables)
3151
3152=== added file 'melange/db/sqlalchemy/migrate_repo/versions/011_add_tenant_id_to_ip_blocks.py'
3153--- melange/db/sqlalchemy/migrate_repo/versions/011_add_tenant_id_to_ip_blocks.py 1970-01-01 00:00:00 +0000
3154+++ melange/db/sqlalchemy/migrate_repo/versions/011_add_tenant_id_to_ip_blocks.py 2011-08-25 11:33:54 +0000
3155@@ -0,0 +1,33 @@
3156+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3157+
3158+# Copyright 2011 OpenStack LLC.
3159+# All Rights Reserved.
3160+#
3161+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3162+# not use this file except in compliance with the License. You may obtain
3163+# a copy of the License at
3164+#
3165+# http://www.apache.org/licenses/LICENSE-2.0
3166+#
3167+# Unless required by applicable law or agreed to in writing, software
3168+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3169+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3170+# License for the specific language governing permissions and limitations
3171+# under the License.
3172+from sqlalchemy.schema import Column
3173+from sqlalchemy.schema import MetaData
3174+from sqlalchemy.schema import Table
3175+
3176+from melange.db.sqlalchemy.migrate_repo.schema import String
3177+
3178+
3179+def upgrade(migrate_engine):
3180+ meta = MetaData()
3181+ meta.bind = migrate_engine
3182+ Column('tenant_id', String(255)).create(Table('ip_blocks', meta))
3183+
3184+
3185+def downgrade(migrate_engine):
3186+ meta = MetaData()
3187+ meta.bind = migrate_engine
3188+ Table('ip_blocks', meta, autoload=True).columns["tenant_id"].drop()
3189
3190=== added file 'melange/db/sqlalchemy/migrate_repo/versions/012_add_tenant_id_to_policies.py'
3191--- melange/db/sqlalchemy/migrate_repo/versions/012_add_tenant_id_to_policies.py 1970-01-01 00:00:00 +0000
3192+++ melange/db/sqlalchemy/migrate_repo/versions/012_add_tenant_id_to_policies.py 2011-08-25 11:33:54 +0000
3193@@ -0,0 +1,33 @@
3194+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3195+
3196+# Copyright 2011 OpenStack LLC.
3197+# All Rights Reserved.
3198+#
3199+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3200+# not use this file except in compliance with the License. You may obtain
3201+# a copy of the License at
3202+#
3203+# http://www.apache.org/licenses/LICENSE-2.0
3204+#
3205+# Unless required by applicable law or agreed to in writing, software
3206+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3207+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3208+# License for the specific language governing permissions and limitations
3209+# under the License.
3210+from sqlalchemy.schema import Column
3211+from sqlalchemy.schema import MetaData
3212+from sqlalchemy.schema import Table
3213+
3214+from melange.db.sqlalchemy.migrate_repo.schema import String
3215+
3216+
3217+def upgrade(migrate_engine):
3218+ meta = MetaData()
3219+ meta.bind = migrate_engine
3220+ Column('tenant_id', String(255)).create(Table('policies', meta))
3221+
3222+
3223+def downgrade(migrate_engine):
3224+ meta = MetaData()
3225+ meta.bind = migrate_engine
3226+ Table('policies', meta, autoload=True).columns["tenant_id"].drop()
3227
3228=== added file 'melange/db/sqlalchemy/migrate_repo/versions/013_add_parent_id_to_ip_blocks.py'
3229--- melange/db/sqlalchemy/migrate_repo/versions/013_add_parent_id_to_ip_blocks.py 1970-01-01 00:00:00 +0000
3230+++ melange/db/sqlalchemy/migrate_repo/versions/013_add_parent_id_to_ip_blocks.py 2011-08-25 11:33:54 +0000
3231@@ -0,0 +1,36 @@
3232+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3233+
3234+# Copyright 2011 OpenStack LLC.
3235+# All Rights Reserved.
3236+#
3237+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3238+# not use this file except in compliance with the License. You may obtain
3239+# a copy of the License at
3240+#
3241+# http://www.apache.org/licenses/LICENSE-2.0
3242+#
3243+# Unless required by applicable law or agreed to in writing, software
3244+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3245+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3246+# License for the specific language governing permissions and limitations
3247+# under the License.
3248+from sqlalchemy.schema import Column
3249+from sqlalchemy.schema import ForeignKeyConstraint
3250+from sqlalchemy.schema import MetaData
3251+
3252+from melange.db.sqlalchemy.migrate_repo.schema import String
3253+from melange.db.sqlalchemy.migrate_repo.schema import Table
3254+
3255+
3256+def upgrade(migrate_engine):
3257+ meta = MetaData()
3258+ meta.bind = migrate_engine
3259+ ip_blocks_table = Table('ip_blocks', meta, autoload=True)
3260+ Column('parent_id', String(36), nullable=True).create(ip_blocks_table)
3261+ ForeignKeyConstraint([ip_blocks_table.c.parent_id], [ip_blocks_table.c.id])
3262+
3263+
3264+def downgrade(migrate_engine):
3265+ meta = MetaData()
3266+ meta.bind = migrate_engine
3267+ Table('ip_blocks', meta, autoload=True).columns["parent_id"].drop()
3268
3269=== added file 'melange/db/sqlalchemy/migrate_repo/versions/014_add_is_full_to_ip_blocks.py'
3270--- melange/db/sqlalchemy/migrate_repo/versions/014_add_is_full_to_ip_blocks.py 1970-01-01 00:00:00 +0000
3271+++ melange/db/sqlalchemy/migrate_repo/versions/014_add_is_full_to_ip_blocks.py 2011-08-25 11:33:54 +0000
3272@@ -0,0 +1,35 @@
3273+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3274+
3275+# Copyright 2011 OpenStack LLC.
3276+# All Rights Reserved.
3277+#
3278+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3279+# not use this file except in compliance with the License. You may obtain
3280+# a copy of the License at
3281+#
3282+# http://www.apache.org/licenses/LICENSE-2.0
3283+#
3284+# Unless required by applicable law or agreed to in writing, software
3285+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3286+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3287+# License for the specific language governing permissions and limitations
3288+# under the License.
3289+from sqlalchemy.schema import Column
3290+from sqlalchemy.schema import MetaData
3291+from sqlalchemy.schema import Table
3292+
3293+from melange.db.sqlalchemy.migrate_repo.schema import Boolean
3294+
3295+
3296+def upgrade(migrate_engine):
3297+ meta = MetaData()
3298+ meta.bind = migrate_engine
3299+
3300+ ip_block_table = Table('ip_blocks', meta)
3301+ Column('is_full', Boolean()).create(ip_block_table)
3302+
3303+
3304+def downgrade(migrate_engine):
3305+ meta = MetaData()
3306+ meta.bind = migrate_engine
3307+ Table('ip_blocks', meta, autoload=True).columns["is_full"].drop()
3308
3309=== added file 'melange/db/sqlalchemy/migrate_repo/versions/015_gateway_and_broadcast_addresses_to_ip_block.py'
3310--- melange/db/sqlalchemy/migrate_repo/versions/015_gateway_and_broadcast_addresses_to_ip_block.py 1970-01-01 00:00:00 +0000
3311+++ melange/db/sqlalchemy/migrate_repo/versions/015_gateway_and_broadcast_addresses_to_ip_block.py 2011-08-25 11:33:54 +0000
3312@@ -0,0 +1,38 @@
3313+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3314+
3315+# Copyright 2011 OpenStack LLC.
3316+# All Rights Reserved.
3317+#
3318+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3319+# not use this file except in compliance with the License. You may obtain
3320+# a copy of the License at
3321+#
3322+# http://www.apache.org/licenses/LICENSE-2.0
3323+#
3324+# Unless required by applicable law or agreed to in writing, software
3325+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3326+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3327+# License for the specific language governing permissions and limitations
3328+# under the License.
3329+from sqlalchemy.schema import Column
3330+from sqlalchemy.schema import MetaData
3331+from sqlalchemy.schema import Table
3332+
3333+from melange.db.sqlalchemy.migrate_repo.schema import String
3334+
3335+
3336+def upgrade(migrate_engine):
3337+ meta = MetaData()
3338+ meta.bind = migrate_engine
3339+
3340+ ip_block_table = Table('ip_blocks', meta)
3341+ Column('broadcast_address', String(255)).create(ip_block_table)
3342+ Column('gateway_address', String(255)).create(ip_block_table)
3343+
3344+
3345+def downgrade(migrate_engine):
3346+ meta = MetaData()
3347+ meta.bind = migrate_engine
3348+ ip_block_table = Table('ip_blocks', meta, autoload=True)
3349+ ip_block_table.columns["broadcast_address"].drop()
3350+ ip_block_table.columns["gateway_address"].drop()
3351
3352=== added file 'melange/db/sqlalchemy/migrate_repo/versions/016_add_deallocated_at_to_ip_addresses.py'
3353--- melange/db/sqlalchemy/migrate_repo/versions/016_add_deallocated_at_to_ip_addresses.py 1970-01-01 00:00:00 +0000
3354+++ melange/db/sqlalchemy/migrate_repo/versions/016_add_deallocated_at_to_ip_addresses.py 2011-08-25 11:33:54 +0000
3355@@ -0,0 +1,34 @@
3356+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3357+
3358+# Copyright 2011 OpenStack LLC.
3359+# All Rights Reserved.
3360+#
3361+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3362+# not use this file except in compliance with the License. You may obtain
3363+# a copy of the License at
3364+#
3365+# http://www.apache.org/licenses/LICENSE-2.0
3366+#
3367+# Unless required by applicable law or agreed to in writing, software
3368+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3369+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3370+# License for the specific language governing permissions and limitations
3371+# under the License.
3372+from sqlalchemy.schema import Column
3373+from sqlalchemy.schema import MetaData
3374+from sqlalchemy.schema import Table
3375+
3376+from melange.db.sqlalchemy.migrate_repo.schema import DateTime
3377+
3378+
3379+def upgrade(migrate_engine):
3380+ meta = MetaData()
3381+ meta.bind = migrate_engine
3382+ Column('deallocated_at', DateTime()).create(\
3383+ Table('ip_addresses', meta))
3384+
3385+
3386+def downgrade(migrate_engine):
3387+ meta = MetaData()
3388+ meta.bind = migrate_engine
3389+ Table('ip_addresses', meta, autoload=True).columns["deallocated_at"].drop()
3390
3391=== added file 'melange/db/sqlalchemy/migrate_repo/versions/017_remove_broadcast_address_and_rename_gateway_address.py'
3392--- melange/db/sqlalchemy/migrate_repo/versions/017_remove_broadcast_address_and_rename_gateway_address.py 1970-01-01 00:00:00 +0000
3393+++ melange/db/sqlalchemy/migrate_repo/versions/017_remove_broadcast_address_and_rename_gateway_address.py 2011-08-25 11:33:54 +0000
3394@@ -0,0 +1,37 @@
3395+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3396+
3397+# Copyright 2011 OpenStack LLC.
3398+# All Rights Reserved.
3399+#
3400+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3401+# not use this file except in compliance with the License. You may obtain
3402+# a copy of the License at
3403+#
3404+# http://www.apache.org/licenses/LICENSE-2.0
3405+#
3406+# Unless required by applicable law or agreed to in writing, software
3407+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3408+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3409+# License for the specific language governing permissions and limitations
3410+# under the License.
3411+from sqlalchemy.schema import Column
3412+from sqlalchemy.schema import MetaData
3413+from sqlalchemy.schema import Table
3414+
3415+from melange.db.sqlalchemy.migrate_repo.schema import String
3416+
3417+
3418+def upgrade(migrate_engine):
3419+ meta = MetaData()
3420+ meta.bind = migrate_engine
3421+ ip_block_table = Table('ip_blocks', meta, autoload=True)
3422+ ip_block_table.columns["broadcast_address"].drop()
3423+ ip_block_table.columns["gateway_address"].alter(name="gateway")
3424+
3425+
3426+def downgrade(migrate_engine):
3427+ meta = MetaData()
3428+ meta.bind = migrate_engine
3429+ ip_block_table = Table('ip_blocks', meta, autoload=True)
3430+ Column('broadcast_address', String(255)).create(ip_block_table)
3431+ ip_block_table.columns["gateway"].alter(name="gateway_address")
3432
3433=== added file 'melange/db/sqlalchemy/migrate_repo/versions/018_add_dns_fields_to_ip_blocks.py'
3434--- melange/db/sqlalchemy/migrate_repo/versions/018_add_dns_fields_to_ip_blocks.py 1970-01-01 00:00:00 +0000
3435+++ melange/db/sqlalchemy/migrate_repo/versions/018_add_dns_fields_to_ip_blocks.py 2011-08-25 11:33:54 +0000
3436@@ -0,0 +1,36 @@
3437+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3438+
3439+# Copyright 2011 OpenStack LLC.
3440+# All Rights Reserved.
3441+#
3442+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3443+# not use this file except in compliance with the License. You may obtain
3444+# a copy of the License at
3445+#
3446+# http://www.apache.org/licenses/LICENSE-2.0
3447+#
3448+# Unless required by applicable law or agreed to in writing, software
3449+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3450+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3451+# License for the specific language governing permissions and limitations
3452+# under the License.
3453+from sqlalchemy.schema import Column
3454+from sqlalchemy.schema import MetaData
3455+
3456+from melange.db.sqlalchemy.migrate_repo.schema import String
3457+from melange.db.sqlalchemy.migrate_repo.schema import Table
3458+
3459+
3460+def upgrade(migrate_engine):
3461+ meta = MetaData()
3462+ meta.bind = migrate_engine
3463+ ip_blocks_table = Table('ip_blocks', meta, autoload=True)
3464+ Column('dns1', String(255), nullable=True).create(ip_blocks_table)
3465+ Column('dns2', String(255), nullable=True).create(ip_blocks_table)
3466+
3467+
3468+def downgrade(migrate_engine):
3469+ meta = MetaData()
3470+ meta.bind = migrate_engine
3471+ Table('ip_blocks', meta, autoload=True).columns["dns1"].drop()
3472+ Table('ip_blocks', meta, autoload=True).columns["dns2"].drop()
3473
3474=== added file 'melange/db/sqlalchemy/migrate_repo/versions/__init__.py'
3475--- melange/db/sqlalchemy/migrate_repo/versions/__init__.py 1970-01-01 00:00:00 +0000
3476+++ melange/db/sqlalchemy/migrate_repo/versions/__init__.py 2011-08-25 11:33:54 +0000
3477@@ -0,0 +1,18 @@
3478+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3479+
3480+# Copyright 2011 OpenStack LLC.
3481+# All Rights Reserved.
3482+#
3483+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3484+# not use this file except in compliance with the License. You may obtain
3485+# a copy of the License at
3486+#
3487+# http://www.apache.org/licenses/LICENSE-2.0
3488+#
3489+# Unless required by applicable law or agreed to in writing, software
3490+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3491+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3492+# License for the specific language governing permissions and limitations
3493+# under the License.
3494+
3495+# template repository default versions module
3496
3497=== added file 'melange/db/sqlalchemy/migration.py'
3498--- melange/db/sqlalchemy/migration.py 1970-01-01 00:00:00 +0000
3499+++ melange/db/sqlalchemy/migration.py 2011-08-25 11:33:54 +0000
3500@@ -0,0 +1,124 @@
3501+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3502+
3503+# Copyright 2011 OpenStack LLC.
3504+# All Rights Reserved.
3505+#
3506+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3507+# not use this file except in compliance with the License. You may obtain
3508+# a copy of the License at
3509+#
3510+# http://www.apache.org/licenses/LICENSE-2.0
3511+#
3512+# Unless required by applicable law or agreed to in writing, software
3513+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3514+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3515+# License for the specific language governing permissions and limitations
3516+# under the License.
3517+import logging
3518+import os
3519+
3520+from migrate.versioning import api as versioning_api
3521+# See LP bug #719834. sqlalchemy-migrate changed location of
3522+# exceptions.py after 0.6.0.
3523+try:
3524+ from migrate.versioning import exceptions as versioning_exceptions
3525+except ImportError:
3526+ from migrate import exceptions as versioning_exceptions
3527+
3528+from melange.common import exception
3529+
3530+
3531+logger = logging.getLogger('melange.db.migration')
3532+
3533+
3534+def db_version(options):
3535+ """Return the database's current migration number
3536+
3537+ :param options: options dict
3538+ :retval version number
3539+ """
3540+ repo_path = get_migrate_repo_path()
3541+ sql_connection = options['sql_connection']
3542+ try:
3543+ return versioning_api.db_version(sql_connection, repo_path)
3544+ except versioning_exceptions.DatabaseNotControlledError, e:
3545+ msg = ("database '%(sql_connection)s' is not under migration control"
3546+ % locals())
3547+ raise exception.DatabaseMigrationError(msg)
3548+
3549+
3550+def upgrade(options, version=None):
3551+ """Upgrade the database's current migration level
3552+
3553+ :param options: options dict
3554+ :param version: version to upgrade (defaults to latest)
3555+ :retval version number
3556+ """
3557+ db_version(options) # Ensure db is under migration control
3558+ repo_path = get_migrate_repo_path()
3559+ sql_connection = options['sql_connection']
3560+ version_str = version or 'latest'
3561+ logger.info("Upgrading %(sql_connection)s to version %(version_str)s" %
3562+ locals())
3563+ return versioning_api.upgrade(sql_connection, repo_path, version)
3564+
3565+
3566+def downgrade(options, version):
3567+ """Downgrade the database's current migration level
3568+
3569+ :param options: options dict
3570+ :param version: version to downgrade to
3571+ :retval version number
3572+ """
3573+ db_version(options) # Ensure db is under migration control
3574+ repo_path = get_migrate_repo_path()
3575+ sql_connection = options['sql_connection']
3576+ logger.info("Downgrading %(sql_connection)s to version %(version)s" %
3577+ locals())
3578+ return versioning_api.downgrade(sql_connection, repo_path, version)
3579+
3580+
3581+def version_control(options):
3582+ """Place a database under migration control
3583+
3584+ :param options: options dict
3585+ """
3586+ sql_connection = options['sql_connection']
3587+ try:
3588+ _version_control(options)
3589+ except versioning_exceptions.DatabaseAlreadyControlledError, e:
3590+ msg = ("database '%(sql_connection)s' is already under migration "
3591+ "control" % locals())
3592+ raise exception.DatabaseMigrationError(msg)
3593+
3594+
3595+def _version_control(options):
3596+ """Place a database under migration control
3597+
3598+ :param options: options dict
3599+ """
3600+ repo_path = get_migrate_repo_path()
3601+ sql_connection = options['sql_connection']
3602+ return versioning_api.version_control(sql_connection, repo_path)
3603+
3604+
3605+def db_sync(options, version=None):
3606+ """Place a database under migration control and perform an upgrade
3607+
3608+ :param options: options dict
3609+ :retval version number
3610+ """
3611+ try:
3612+ _version_control(options)
3613+ except versioning_exceptions.DatabaseAlreadyControlledError, e:
3614+ pass
3615+
3616+ upgrade(options, version=version)
3617+
3618+
3619+def get_migrate_repo_path():
3620+ """Get the path for the migrate repository."""
3621+ path = os.path.join(os.path.abspath(os.path.dirname(__file__)),
3622+ 'migrate_repo')
3623+ assert os.path.exists(path)
3624+ return path
3625
3626=== added file 'melange/db/sqlalchemy/session.py'
3627--- melange/db/sqlalchemy/session.py 1970-01-01 00:00:00 +0000
3628+++ melange/db/sqlalchemy/session.py 2011-08-25 11:33:54 +0000
3629@@ -0,0 +1,91 @@
3630+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3631+
3632+# Copyright 2011 OpenStack LLC.
3633+# All Rights Reserved.
3634+#
3635+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3636+# not use this file except in compliance with the License. You may obtain
3637+# a copy of the License at
3638+#
3639+# http://www.apache.org/licenses/LICENSE-2.0
3640+#
3641+# Unless required by applicable law or agreed to in writing, software
3642+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3643+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3644+# License for the specific language governing permissions and limitations
3645+# under the License.
3646+
3647+_ENGINE = None
3648+_MAKER = None
3649+
3650+import contextlib
3651+import logging
3652+from sqlalchemy import create_engine
3653+from sqlalchemy import MetaData
3654+from sqlalchemy.orm import sessionmaker
3655+
3656+from melange import ipam
3657+from melange.common import config
3658+from melange.db.sqlalchemy import mappers
3659+
3660+
3661+def configure_db(options):
3662+ configure_sqlalchemy_log(options)
3663+ global _ENGINE
3664+ if not _ENGINE:
3665+ _ENGINE = _create_engine(options)
3666+ mappers.map(_ENGINE, ipam.models.persisted_models())
3667+
3668+
3669+def configure_sqlalchemy_log(options):
3670+ debug = config.get_option(options,
3671+ 'debug', type='bool', default=False)
3672+ verbose = config.get_option(options,
3673+ 'verbose', type='bool', default=False)
3674+ logger = logging.getLogger('sqlalchemy.engine')
3675+ if debug:
3676+ logger.setLevel(logging.DEBUG)
3677+ elif verbose:
3678+ logger.setLevel(logging.INFO)
3679+
3680+
3681+def _create_engine(options):
3682+ timeout = config.get_option(options,
3683+ 'sql_idle_timeout', type='int', default=3600)
3684+ return create_engine(options['sql_connection'],
3685+ pool_recycle=timeout)
3686+
3687+
3688+def get_session(autocommit=True, expire_on_commit=False):
3689+ """Helper method to grab session"""
3690+ global _MAKER, _ENGINE
3691+ if not _MAKER:
3692+ assert _ENGINE
3693+ _MAKER = sessionmaker(bind=_ENGINE,
3694+ autocommit=autocommit,
3695+ expire_on_commit=expire_on_commit)
3696+ return _MAKER()
3697+
3698+
3699+def raw_query(model, autocommit=True, expire_on_commit=False):
3700+ return get_session(autocommit, expire_on_commit).query(model)
3701+
3702+
3703+def clean_db():
3704+ global _ENGINE
3705+ meta = MetaData()
3706+ meta.reflect(bind=_ENGINE)
3707+ with contextlib.closing(_ENGINE.connect()) as con:
3708+ trans = con.begin()
3709+ for table in reversed(meta.sorted_tables):
3710+ if table.name != "migrate_version":
3711+ con.execute(table.delete())
3712+ trans.commit()
3713+
3714+
3715+def drop_db(options):
3716+ meta = MetaData()
3717+ engine = _create_engine(options)
3718+ meta.bind = engine
3719+ meta.reflect()
3720+ meta.drop_all()
3721
3722=== added directory 'melange/extensions'
3723=== added file 'melange/extensions/__init__.py'
3724--- melange/extensions/__init__.py 1970-01-01 00:00:00 +0000
3725+++ melange/extensions/__init__.py 2011-08-25 11:33:54 +0000
3726@@ -0,0 +1,16 @@
3727+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3728+
3729+# Copyright 2011 OpenStack LLC.
3730+# All Rights Reserved.
3731+#
3732+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3733+# not use this file except in compliance with the License. You may obtain
3734+# a copy of the License at
3735+#
3736+# http://www.apache.org/licenses/LICENSE-2.0
3737+#
3738+# Unless required by applicable law or agreed to in writing, software
3739+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3740+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3741+# License for the specific language governing permissions and limitations
3742+# under the License.
3743
3744=== added directory 'melange/ipam'
3745=== added file 'melange/ipam/__init__.py'
3746--- melange/ipam/__init__.py 1970-01-01 00:00:00 +0000
3747+++ melange/ipam/__init__.py 2011-08-25 11:33:54 +0000
3748@@ -0,0 +1,16 @@
3749+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3750+
3751+# Copyright 2011 OpenStack LLC.
3752+# All Rights Reserved.
3753+#
3754+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3755+# not use this file except in compliance with the License. You may obtain
3756+# a copy of the License at
3757+#
3758+# http://www.apache.org/licenses/LICENSE-2.0
3759+#
3760+# Unless required by applicable law or agreed to in writing, software
3761+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3762+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3763+# License for the specific language governing permissions and limitations
3764+# under the License.
3765
3766=== added file 'melange/ipam/client.py'
3767--- melange/ipam/client.py 1970-01-01 00:00:00 +0000
3768+++ melange/ipam/client.py 2011-08-25 11:33:54 +0000
3769@@ -0,0 +1,178 @@
3770+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3771+
3772+# Copyright 2011 OpenStack LLC.
3773+# All Rights Reserved.
3774+#
3775+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3776+# not use this file except in compliance with the License. You may obtain
3777+# a copy of the License at
3778+#
3779+# http://www.apache.org/licenses/LICENSE-2.0
3780+#
3781+# Unless required by applicable law or agreed to in writing, software
3782+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3783+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3784+# License for the specific language governing permissions and limitations
3785+# under the License.
3786+import json
3787+
3788+from melange.common.utils import remove_nones
3789+
3790+
3791+class Resource(object):
3792+
3793+ def __init__(self, path, name, client, auth_client, tenant_id=None):
3794+ if tenant_id:
3795+ path = "tenants/{0}/{1}".format(tenant_id, path)
3796+ self.path = "/v0.1/ipam/" + path
3797+ self.name = name
3798+ self.client = client
3799+ self.auth_client = auth_client
3800+
3801+ def create(self, **kwargs):
3802+ return self.request("POST", self.path,
3803+ body=json.dumps({self.name: kwargs}))
3804+
3805+ def update(self, id, **kwargs):
3806+ return self.request("PUT", self._member_path(id),
3807+ body=json.dumps({self.name: remove_nones(kwargs)}))
3808+
3809+ def all(self):
3810+ return self.request("GET", self.path)
3811+
3812+ def find(self, id):
3813+ return self.request("GET", self._member_path(id))
3814+
3815+ def delete(self, id):
3816+ return self.request("DELETE", self._member_path(id))
3817+
3818+ def _member_path(self, id):
3819+ return "{0}/{1}".format(self.path, id)
3820+
3821+ def request(self, method, path, body_params=None, **kwargs):
3822+ kwargs['headers'] = {'X-AUTH-TOKEN': self.auth_client.get_token(),
3823+ 'Content-Type': "application/json"}
3824+ response = self.client.do_request(method, path, **kwargs)
3825+ return response.read()
3826+
3827+
3828+class IpBlockClient(object):
3829+
3830+ def __init__(self, client, auth_client, tenant_id=None):
3831+ self.resource = Resource("ip_blocks", "ip_block",
3832+ client, auth_client, tenant_id)
3833+
3834+ def create(self, type, cidr, network_id=None, policy_id=None):
3835+ return self.resource.create(type=type, cidr=cidr,
3836+ network_id=network_id, policy_id=policy_id)
3837+
3838+ def list(self):
3839+ return self.resource.all()
3840+
3841+ def show(self, id):
3842+ return self.resource.find(id)
3843+
3844+ def update(self, id, network_id=None, policy_id=None):
3845+ return self.resource.update(id, network_id=network_id,
3846+ policy_id=policy_id)
3847+
3848+ def delete(self, id):
3849+ return self.resource.delete(id)
3850+
3851+
3852+class SubnetClient(object):
3853+
3854+ def __init__(self, client, auth_client, tenant_id=None):
3855+ self.tenant_id = tenant_id
3856+ self.client = client
3857+ self.auth_client = auth_client
3858+
3859+ def _resource(self, parent_id):
3860+ return Resource("ip_blocks/{0}/subnets".format(parent_id), "subnet",
3861+ self.client, self.auth_client, self.tenant_id)
3862+
3863+ def create(self, parent_id, cidr, network_id=None):
3864+ return self._resource(parent_id).create(cidr=cidr,
3865+ network_id=network_id)
3866+
3867+ def list(self, parent_id):
3868+ return self._resource(parent_id).all()
3869+
3870+
3871+class PolicyClient(object):
3872+
3873+ def __init__(self, client, auth_client, tenant_id=None):
3874+ self.resource = Resource("policies", "policy", client,
3875+ auth_client, tenant_id)
3876+
3877+ def create(self, name, description=None):
3878+ return self.resource.create(name=name, description=description)
3879+
3880+ def update(self, id, name, description=None):
3881+ return self.resource.update(id, name=name, description=description)
3882+
3883+ def list(self):
3884+ return self.resource.all()
3885+
3886+ def show(self, id):
3887+ return self.resource.find(id)
3888+
3889+ def delete(self, id):
3890+ return self.resource.delete(id)
3891+
3892+
3893+class UnusableIpRangesClient(object):
3894+
3895+ def __init__(self, client, auth_client, tenant_id=None):
3896+ self.client = client
3897+ self.auth_client = auth_client
3898+ self.tenant_id = tenant_id
3899+
3900+ def _resource(self, policy_id):
3901+ return Resource("policies/{0}/unusable_ip_ranges".format(policy_id),
3902+ "ip_range", self.client, self.auth_client,
3903+ self.tenant_id)
3904+
3905+ def create(self, policy_id, offset, length):
3906+ return self._resource(policy_id).create(offset=offset, length=length)
3907+
3908+ def update(self, policy_id, id, offset=None, length=None):
3909+ return self._resource(policy_id).update(id, offset=offset,
3910+ length=length)
3911+
3912+ def list(self, policy_id):
3913+ return self._resource(policy_id).all()
3914+
3915+ def show(self, policy_id, id):
3916+ return self. _resource(policy_id).find(id)
3917+
3918+ def delete(self, policy_id, id):
3919+ return self._resource(policy_id).delete(id)
3920+
3921+
3922+class UnusableIpOctetsClient(object):
3923+
3924+ def __init__(self, client, auth_client, tenant_id=None):
3925+ self.client = client
3926+ self.auth_client = auth_client
3927+ self.tenant_id = tenant_id
3928+
3929+ def _resource(self, policy_id):
3930+ return Resource("policies/{0}/unusable_ip_octets".format(policy_id),
3931+ "ip_octet", self.client, self.auth_client,
3932+ self.tenant_id)
3933+
3934+ def create(self, policy_id, octet):
3935+ return self._resource(policy_id).create(octet=octet)
3936+
3937+ def update(self, policy_id, id, octet=None):
3938+ return self._resource(policy_id).update(id, octet=octet)
3939+
3940+ def list(self, policy_id):
3941+ return self._resource(policy_id).all()
3942+
3943+ def show(self, policy_id, id):
3944+ return self._resource(policy_id).find(id)
3945+
3946+ def delete(self, policy_id, id):
3947+ return self._resource(policy_id).delete(id)
3948
3949=== added file 'melange/ipam/models.py'
3950--- melange/ipam/models.py 1970-01-01 00:00:00 +0000
3951+++ melange/ipam/models.py 2011-08-25 11:33:54 +0000
3952@@ -0,0 +1,820 @@
3953+# vim: tabstop=4 shiftwidth=4 softtabstop=4
3954+
3955+# Copyright 2010-2011 OpenStack LLC.
3956+# All Rights Reserved.
3957+#
3958+# Licensed under the Apache License, Version 2.0 (the "License"); you may
3959+# not use this file except in compliance with the License. You may obtain
3960+# a copy of the License at
3961+#
3962+# http: //www.apache.org/licenses/LICENSE-2.0
3963+#
3964+# Unless required by applicable law or agreed to in writing, software
3965+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3966+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3967+# License for the specific language governing permissions and limitations
3968+# under the License.
3969+
3970+"""
3971+SQLAlchemy models for Melange data
3972+"""
3973+
3974+import netaddr
3975+from datetime import timedelta
3976+from netaddr.strategy.ipv6 import ipv6_verbose
3977+from openstack.common.utils import bool_from_string
3978+
3979+from melange.common import utils
3980+from melange.common.config import Config
3981+from melange.common.exception import MelangeError
3982+from melange.common.utils import cached_property
3983+from melange.common.utils import exclude
3984+from melange.common.utils import find
3985+from melange.db import db_api
3986+
3987+
3988+class Query(object):
3989+
3990+ def __init__(self, model, **conditions):
3991+ self._model = model
3992+ self._conditions = conditions
3993+
3994+ def all(self):
3995+ return db_api.find_all_by(self._model, **self._conditions)
3996+
3997+ def __iter__(self):
3998+ return iter(self.all())
3999+
4000+ def update(self, **values):
4001+ db_api.update_all(self._model, self._conditions, values)
4002+
4003+ def delete(self):
4004+ db_api.delete_all(self._model, **self._conditions)
4005+
4006+ def limit(self, limit=200, marker=None, marker_column=None):
4007+ return db_api.find_all_by_limit(self._model, self._conditions,
4008+ limit=limit, marker=marker,
4009+ marker_column=marker_column)
4010+
4011+ def paginated_collection(self, limit=200, marker=None, marker_column=None):
4012+ collection = self.limit(int(limit) + 1, marker, marker_column)
4013+ if len(collection) > int(limit):
4014+ return (collection[0:-1], collection[-2]['id'])
4015+ return (collection, None)
4016+
4017+
4018+class Converter(object):
4019+ data_type_converters = {'integer': lambda x: int(x),
4020+ 'boolean': lambda value: bool_from_string(value)}
4021+
4022+ def __init__(self, data_type):
4023+ self.data_type = data_type
4024+
4025+ def convert(self, value):
4026+ return self.data_type_converters[self.data_type](value)
4027+
4028+
4029+class ModelBase(object):
4030+ _columns = {}
4031+ _auto_generated_attrs = ["id", "created_at", "updated_at"]
4032+ _data_fields = []
4033+
4034+ @classmethod
4035+ def create(cls, **values):
4036+ values['id'] = utils.guid()
4037+ values['created_at'] = utils.utcnow()
4038+ instance = cls(**values)
4039+ return instance.save()
4040+
4041+ def save(self):
4042+ if not self.is_valid():
4043+ raise InvalidModelError(self.errors)
4044+ self._convert_columns_to_proper_type()
4045+ self._before_save()
4046+ self['updated_at'] = utils.utcnow()
4047+ return db_api.save(self)
4048+
4049+ def delete(self):
4050+ db_api.delete(self)
4051+
4052+ def __init__(self, **kwargs):
4053+ self.merge_attributes(kwargs)
4054+
4055+ def _validate_columns_type(self):
4056+ for column_name, data_type in self._columns.iteritems():
4057+ try:
4058+ Converter(data_type).convert(self[column_name])
4059+ except (TypeError, ValueError):
4060+ self._add_error(column_name,
4061+ _("%(column_name)s should be of type %(data_type)s")
4062+ % locals())
4063+
4064+ def _validate(self):
4065+ pass
4066+
4067+ def _before_validate(self):
4068+ pass
4069+
4070+ def _before_save(self):
4071+ pass
4072+
4073+ def _convert_columns_to_proper_type(self):
4074+ for column_name, data_type in self._columns.iteritems():
4075+ self[column_name] = Converter(data_type).convert(self[column_name])
4076+
4077+ def is_valid(self):
4078+ self.errors = {}
4079+ self._validate_columns_type()
4080+ self._before_validate()
4081+ self._validate()
4082+ return self.errors == {}
4083+
4084+ def _validate_presence_of(self, attribute_name):
4085+ if (self[attribute_name] in [None, ""]):
4086+ self._add_error(attribute_name,
4087+ _("%(attribute_name)s should be present")
4088+ % locals())
4089+
4090+ def _validate_existence_of(self, attribute, model_class, **conditions):
4091+ model_id = self[attribute]
4092+ conditions['id'] = model_id
4093+ if model_id is not None and model_class.get_by(**conditions) is None:
4094+ conditions_str = ", ".join(["{0} = {1}".format(key, repr(value))
4095+ for key, value in conditions.iteritems()])
4096+ model_class_name = model_class.__name__
4097+ self._add_error(attribute,
4098+ _("%(model_class_name)s with %(conditions_str)s"
4099+ " doesn't exist") % locals())
4100+
4101+ @classmethod
4102+ def find(cls, id):
4103+ return cls.find_by(id=id)
4104+
4105+ @classmethod
4106+ def get(cls, id):
4107+ return cls.get_by(id=id)
4108+
4109+ @classmethod
4110+ def find_by(cls, **conditions):
4111+ model = cls.get_by(**conditions)
4112+ if model == None:
4113+ raise ModelNotFoundError(_("%s Not Found") % cls.__name__)
4114+ return model
4115+
4116+ @classmethod
4117+ def get_by(cls, **kwargs):
4118+ return db_api.find_by(cls, **cls._get_conditions(kwargs))
4119+
4120+ @classmethod
4121+ def _get_conditions(cls, raw_conditions):
4122+ return raw_conditions
4123+
4124+ @classmethod
4125+ def find_all(cls, **kwargs):
4126+ return Query(cls, **cls._get_conditions(kwargs))
4127+
4128+ def merge_attributes(self, values):
4129+ """dict.update() behaviour."""
4130+ for k, v in values.iteritems():
4131+ self[k] = v
4132+
4133+ def update(self, **values):
4134+ attrs = exclude(values, *self._auto_generated_attrs)
4135+ self.merge_attributes(attrs)
4136+ return self.save()
4137+
4138+ def __setitem__(self, key, value):
4139+ setattr(self, key, value)
4140+
4141+ def __getitem__(self, key):
4142+ return getattr(self, key)
4143+
4144+ def __iter__(self):
4145+ self._i = iter(db_api.columns_of(self))
4146+ return self
4147+
4148+ def __eq__(self, other):
4149+ if not hasattr(other, 'id'):
4150+ return False
4151+ return type(other) == type(self) and other.id == self.id
4152+
4153+ def __ne__(self, other):
4154+ return not self == other
4155+
4156+ def __hash__(self):
4157+ return id.__hash__()
4158+
4159+ def next(self):
4160+ n = self._i.next().name
4161+ return n, getattr(self, n)
4162+
4163+ def keys(self):
4164+ return self.__dict__.keys()
4165+
4166+ def values(self):
4167+ return self.__dict__.values()
4168+
4169+ def items(self):
4170+ return self.__dict__.items()
4171+
4172+ def to_dict(self):
4173+ return self.__dict__()
4174+
4175+ def data(self, **options):
4176+ data_fields = self._data_fields + self._auto_generated_attrs
4177+ return dict([(field, self[field])
4178+ for field in data_fields])
4179+
4180+ def _validate_positive_integer(self, attribute_name):
4181+ if(utils.parse_int(self[attribute_name]) < 0):
4182+ self._add_error(attribute_name,
4183+ _("%s should be a positive integer")
4184+ % attribute_name)
4185+
4186+ def _add_error(self, attribute_name, error_message):
4187+ self.errors[attribute_name] = self.errors.get(attribute_name, [])
4188+ self.errors[attribute_name].append(error_message)
4189+
4190+ def _has_error_on(self, attribute):
4191+ return self.errors.get(attribute, None) is not None
4192+
4193+
4194+def ipv6_address_generator_factory(cidr, **kwargs):
4195+ default_generator = "melange.ipv6.tenant_based_generator."\
4196+ "TenantBasedIpV6Generator"
4197+ ip_generator_class_name = Config.get("ipv6_generator", default_generator)
4198+ ip_generator = utils.import_class(ip_generator_class_name)
4199+ required_params = ip_generator.required_params\
4200+ if hasattr(ip_generator, "required_params") else []
4201+ missing_params = set(required_params) - set(kwargs.keys())
4202+ if missing_params:
4203+ raise DataMissingError(_("Required params are missing: %s")
4204+ % (', '.join(missing_params)))
4205+ return ip_generator(cidr, **kwargs)
4206+
4207+
4208+class IpAddressIterator(object):
4209+
4210+ def __init__(self, generator):
4211+ self.generator = generator
4212+
4213+ def __iter__(self):
4214+ return self
4215+
4216+ def next(self):
4217+ return self.generator.next_ip()
4218+
4219+
4220+class IpBlock(ModelBase):
4221+
4222+ _allowed_types = ["private", "public"]
4223+ _data_fields = ['cidr', 'network_id', 'policy_id', 'tenant_id', 'gateway',
4224+ 'parent_id', 'type', 'dns1', 'dns2',
4225+ 'broadcast', 'netmask']
4226+
4227+ @classmethod
4228+ def find_or_allocate_ip(cls, ip_block_id, address):
4229+ block = IpBlock.find(ip_block_id)
4230+ allocated_ip = IpAddress.get_by(ip_block_id=block.id, address=address)
4231+
4232+ if allocated_ip and allocated_ip.locked():
4233+ raise AddressLockedError()
4234+
4235+ return (allocated_ip or block.allocate_ip(address=address))
4236+
4237+ @classmethod
4238+ def find_all_by_policy(cls, policy_id):
4239+ return cls.find_all(policy_id=policy_id)
4240+
4241+ @classmethod
4242+ def allowed_by_policy(cls, ip_block, policy, address):
4243+ return policy == None or policy.allows(ip_block.cidr, address)
4244+
4245+ @classmethod
4246+ def delete_all_deallocated_ips(cls):
4247+ for block in db_api.find_all_blocks_with_deallocated_ips():
4248+ block.update(is_full=False)
4249+ block.delete_deallocated_ips()
4250+
4251+ @property
4252+ def broadcast(self):
4253+ return str(netaddr.IPNetwork(self.cidr).broadcast)
4254+
4255+ @property
4256+ def netmask(self):
4257+ return str(netaddr.IPNetwork(self.cidr).netmask)
4258+
4259+ def is_ipv6(self):
4260+ return netaddr.IPNetwork(self.cidr).version == 6
4261+
4262+ def subnets(self):
4263+ return IpBlock.find_all(parent_id=self.id).all()
4264+
4265+ def siblings(self):
4266+ if not self.parent:
4267+ return []
4268+ return filter(lambda block: block != self, self.parent.subnets())
4269+
4270+ def delete(self):
4271+ for block in self.subnets():
4272+ block.delete()
4273+ IpAddress.find_all(ip_block_id=self.id).delete()
4274+ super(IpBlock, self).delete()
4275+
4276+ def policy(self):
4277+ return Policy.get(self.policy_id)
4278+
4279+ def get_address(self, address):
4280+ return IpAddress.get_by(ip_block_id=self.id, address=address)
4281+
4282+ def addresses(self):
4283+ return IpAddress.find_all(ip_block_id=self.id).all()
4284+
4285+ @cached_property
4286+ def parent(self):
4287+ return IpBlock.get(self.parent_id)
4288+
4289+ def allocate_ip(self, interface_id=None, address=None, **kwargs):
4290+ tenant_id = kwargs.get('tenant_id', None)
4291+ if self.tenant_id and tenant_id and self.tenant_id != tenant_id:
4292+ raise InvalidTenantError(_("Cannot allocate ip address "
4293+ "from differnt tenant's block"))
4294+ if self.subnets():
4295+ raise IpAllocationNotAllowedError(
4296+ _("Non Leaf block can not allocate IPAddress"))
4297+ if self.is_full:
4298+ raise NoMoreAddressesError(_("IpBlock is full"))
4299+
4300+ if address is None:
4301+ address = self._generate_ip_address(**kwargs)
4302+ else:
4303+ self._validate_address(address)
4304+
4305+ if not address:
4306+ self.update(is_full=True)
4307+ raise NoMoreAddressesError(_("IpBlock is full"))
4308+
4309+ return IpAddress.create(address=address, interface_id=interface_id,
4310+ ip_block_id=self.id)
4311+
4312+ def _generate_ip_address(self, **kwargs):
4313+ if(self.is_ipv6()):
4314+ address_generator = ipv6_address_generator_factory(self.cidr,
4315+ **kwargs)
4316+
4317+ return find(lambda address: self.get_address(address) is None,
4318+ IpAddressIterator(address_generator))
4319+ else:
4320+ #TODO: very inefficient way to generate ips,
4321+ #will look at better algos for this
4322+ allocated_addresses = [ip.address for ip in self.addresses()]
4323+ unavailable_addresses = allocated_addresses + [self.gateway,
4324+ self.broadcast]
4325+ policy = self.policy()
4326+ for ip in netaddr.IPNetwork(self.cidr):
4327+ if (IpBlock.allowed_by_policy(self, policy, str(ip))
4328+ and (str(ip) not in unavailable_addresses)):
4329+ return str(ip)
4330+ return None
4331+
4332+ def _validate_address(self, address):
4333+
4334+ if (address in [self.broadcast, self.gateway]
4335+ or (self.get_address(address) is not None)):
4336+ raise DuplicateAddressError()
4337+
4338+ if not self.contains(address):
4339+ raise AddressDoesNotBelongError(
4340+ _("Address does not belong to IpBlock"))
4341+
4342+ policy = self.policy()
4343+ if not IpBlock.allowed_by_policy(self, policy, address):
4344+ raise AddressDisallowedByPolicyError(
4345+ _("Block policy does not allow this address"))
4346+
4347+ def contains(self, address):
4348+ return netaddr.IPAddress(address) in netaddr.IPNetwork(self.cidr)
4349+
4350+ def _overlaps(self, other_block):
4351+ network = netaddr.IPNetwork(self.cidr)
4352+ other_network = netaddr.IPNetwork(other_block.cidr)
4353+ return network in other_network or other_network in network
4354+
4355+ def find_allocated_ip(self, address):
4356+ ip_address = IpAddress.find_by(ip_block_id=self.id, address=address)
4357+ if ip_address == None:
4358+ raise ModelNotFoundError(_("IpAddress Not Found"))
4359+ return ip_address
4360+
4361+ def deallocate_ip(self, address):
4362+ ip_address = IpAddress.find_by(ip_block_id=self.id, address=address)
4363+ if ip_address != None:
4364+ ip_address.deallocate()
4365+
4366+ def delete_deallocated_ips(self):
4367+ db_api.delete_deallocated_ips(
4368+ deallocated_by=self._deallocated_by_date(), ip_block_id=self.id)
4369+
4370+ def _deallocated_by_date(self):
4371+ days_to_keep_ips = Config.get('keep_deallocated_ips_for_days', 2)
4372+ return utils.utcnow() - timedelta(days=days_to_keep_ips)
4373+
4374+ def subnet(self, cidr, network_id=None, tenant_id=None):
4375+ network_id = network_id or self.network_id
4376+ tenant_id = tenant_id or self.tenant_id
4377+ return IpBlock.create(cidr=cidr, network_id=network_id,
4378+ parent_id=self.id, type=self.type,
4379+ tenant_id=tenant_id)
4380+
4381+ def _validate_cidr_format(self):
4382+ if not self._has_valid_cidr():
4383+ self._add_error('cidr', _("cidr is invalid"))
4384+
4385+ def _has_valid_cidr(self):
4386+ try:
4387+ netaddr.IPNetwork(self.cidr)
4388+ return True
4389+ except Exception:
4390+ return False
4391+
4392+ def _validate_cidr_is_within_parent_block_cidr(self):
4393+ parent = self.parent
4394+ if (parent and netaddr.IPNetwork(self.cidr) not in
4395+ netaddr.IPNetwork(parent.cidr)):
4396+ self._add_error('cidr',
4397+ _("cidr should be within parent block's cidr"))
4398+
4399+ def _validate_type(self):
4400+ if not (self.type in self._allowed_types):
4401+ self._add_error('type', _("type should be one among %s") %
4402+ ", ".join(self._allowed_types))
4403+
4404+ def _validate_cidr(self):
4405+ self._validate_cidr_format()
4406+ if not self._has_valid_cidr():
4407+ return
4408+ self._validate_cidr_doesnt_overlap_for_root_public_ip_blocks()
4409+ self._validate_cidr_is_within_parent_block_cidr()
4410+ self._validate_cidr_does_not_overlap_with_siblings()
4411+ if self._is_top_level_block_in_network():
4412+ self._validate_cidr_doesnt_overlap_with_networked_toplevel_blocks()
4413+
4414+ def _validate_cidr_doesnt_overlap_for_root_public_ip_blocks(self):
4415+ if self.type != 'public':
4416+ return
4417+ for block in IpBlock.find_all(type='public', parent_id=None):
4418+ if self != block and self._overlaps(block):
4419+ msg = _("cidr overlaps with public block %s") % block.cidr
4420+ self._add_error('cidr', msg)
4421+ break
4422+
4423+ def _validate_cidr_does_not_overlap_with_siblings(self):
4424+ for sibling in self.siblings():
4425+ if self._overlaps(sibling):
4426+ msg = _("cidr overlaps with sibling %s") % sibling.cidr
4427+ self._add_error('cidr', msg)
4428+ break
4429+
4430+ def networked_top_level_blocks(self):
4431+ if not self.network_id:
4432+ return []
4433+ blocks = db_api.find_all_top_level_blocks_in_network(self.network_id)
4434+ return filter(lambda block: block != self and block != self.parent,
4435+ blocks)
4436+
4437+ def _is_top_level_block_in_network(self):
4438+ return not self.parent or self.network_id != self.parent.network_id
4439+
4440+ def _validate_cidr_doesnt_overlap_with_networked_toplevel_blocks(self):
4441+ for block in self.networked_top_level_blocks():
4442+ if self._overlaps(block):
4443+ self._add_error('cidr', _("cidr overlaps with block %s"
4444+ " in same network") % block.cidr)
4445+ break
4446+
4447+ def _validate_belongs_to_supernet_network(self):
4448+ if(self.parent and self.parent.network_id and
4449+ self.parent.network_id != self.network_id):
4450+ self._add_error('network_id',
4451+ _("network_id should be same as that of parent"))
4452+
4453+ def _validate_belongs_to_supernet_tenant(self):
4454+ if(self.parent and self.parent.tenant_id and
4455+ self.parent.tenant_id != self.tenant_id):
4456+ self._add_error('tenant_id',
4457+ _("tenant_id should be same as that of parent"))
4458+
4459+ def _validate_parent_is_subnettable(self):
4460+ if (self.parent and self.parent.addresses()):
4461+ msg = _("parent is not subnettable since it has allocated ips")
4462+ self._add_error('parent_id', msg)
4463+
4464+ def _validate_type_is_same_within_network(self):
4465+ block = IpBlock.get_by(network_id=self.network_id)
4466+ if(block and block.type != self.type):
4467+ self._add_error('type', _("type should be same within a network"))
4468+
4469+ def _validate(self):
4470+ self._validate_type()
4471+ self._validate_cidr()
4472+ self._validate_existence_of('parent_id', IpBlock, type=self.type)
4473+ self._validate_belongs_to_supernet_network()
4474+ self._validate_belongs_to_supernet_tenant()
4475+ self._validate_parent_is_subnettable()
4476+ self._validate_existence_of('policy_id', Policy)
4477+ self._validate_type_is_same_within_network()
4478+
4479+ def _convert_cidr_to_lowest_address(self):
4480+ if self._has_valid_cidr():
4481+ self.cidr = str(netaddr.IPNetwork(self.cidr).cidr)
4482+
4483+ def _before_validate(self):
4484+ self._convert_cidr_to_lowest_address()
4485+
4486+ def _before_save(self):
4487+ self.gateway = self.gateway or str(netaddr.IPNetwork(self.cidr)[1])
4488+ self.dns1 = self.dns1 or Config.get("dns1")
4489+ self.dns2 = self.dns2 or Config.get("dns2")
4490+
4491+
4492+class IpAddress(ModelBase):
4493+
4494+ _data_fields = ['ip_block_id', 'address', 'interface_id', 'version']
4495+
4496+ @classmethod
4497+ def _get_conditions(cls, raw_conditions):
4498+ conditions = raw_conditions.copy()
4499+ if 'address' in conditions:
4500+ conditions['address'] = cls._formatted(conditions['address'])
4501+ return conditions
4502+
4503+ @classmethod
4504+ def _formatted(cls, address):
4505+ return netaddr.IPAddress(address).format(dialect=ipv6_verbose)
4506+
4507+ @classmethod
4508+ def find_all_by_network(cls, network_id, **conditions):
4509+ return db_api.find_all_ips_in_network(network_id, **conditions)
4510+
4511+ def _before_save(self):
4512+ self.address = self._formatted(self.address)
4513+
4514+ def ip_block(self):
4515+ return IpBlock.get(self.ip_block_id)
4516+
4517+ def add_inside_locals(self, ip_addresses):
4518+ db_api.save_nat_relationships([
4519+ {'inside_global_address_id': self.id,
4520+ 'inside_local_address_id': local_address.id}
4521+ for local_address in ip_addresses])
4522+
4523+ def deallocate(self):
4524+ return self.update(marked_for_deallocation=True,
4525+ deallocated_at=utils.utcnow())
4526+
4527+ def restore(self):
4528+ self.update(marked_for_deallocation=False, deallocated_at=None)
4529+
4530+ def inside_globals(self, **kwargs):
4531+ return db_api.find_inside_globals_for(self.id, **kwargs)
4532+
4533+ def add_inside_globals(self, ip_addresses):
4534+ return db_api.save_nat_relationships([
4535+ {'inside_global_address_id': global_address.id,
4536+ 'inside_local_address_id': self.id}
4537+ for global_address in ip_addresses])
4538+
4539+ def inside_locals(self, **kwargs):
4540+ return db_api.find_inside_locals_for(self.id, **kwargs)
4541+
4542+ def remove_inside_globals(self, inside_global_address=None):
4543+ return db_api.remove_inside_globals(self.id, inside_global_address)
4544+
4545+ def remove_inside_locals(self, inside_local_address=None):
4546+ return db_api.remove_inside_locals(self.id, inside_local_address)
4547+
4548+ def locked(self):
4549+ return self.marked_for_deallocation
4550+
4551+ @property
4552+ def version(self):
4553+ return netaddr.IPAddress(self.address).version
4554+
4555+ def data(self, **options):
4556+ data = super(IpAddress, self).data(**options)
4557+ if options.get('with_ip_block', False):
4558+ data['ip_block'] = self.ip_block().data()
4559+ return data
4560+
4561+ def __str__(self):
4562+ return self.address
4563+
4564+
4565+class Policy(ModelBase):
4566+
4567+ _data_fields = ['name', 'description', 'tenant_id']
4568+
4569+ def _validate(self):
4570+ self._validate_presence_of('name')
4571+
4572+ def delete(self):
4573+ IpRange.find_all(policy_id=self.id).delete()
4574+ IpOctet.find_all(policy_id=self.id).delete()
4575+ IpBlock.find_all(policy_id=self.id).update(policy_id=None)
4576+ super(Policy, self).delete()
4577+
4578+ def create_unusable_range(self, **attributes):
4579+ attributes['policy_id'] = self.id
4580+ return IpRange.create(**attributes)
4581+
4582+ def create_unusable_ip_octet(self, **attributes):
4583+ attributes['policy_id'] = self.id
4584+ return IpOctet.create(**attributes)
4585+
4586+ @cached_property
4587+ def unusable_ip_ranges(self):
4588+ return IpRange.find_all(policy_id=self.id).all()
4589+
4590+ @cached_property
4591+ def unusable_ip_octets(self):
4592+ return IpOctet.find_all(policy_id=self.id).all()
4593+
4594+ def allows(self, cidr, address):
4595+ if (any(ip_octet.applies_to(address)
4596+ for ip_octet in self.unusable_ip_octets)):
4597+ return False
4598+ return not any(ip_range.contains(cidr, address)
4599+ for ip_range in self.unusable_ip_ranges)
4600+
4601+ def find_ip_range(self, ip_range_id):
4602+ return IpRange.find_by(id=ip_range_id, policy_id=self.id)
4603+
4604+ def find_ip_octet(self, ip_octet_id):
4605+ return IpOctet.find_by(id=ip_octet_id, policy_id=self.id)
4606+
4607+
4608+class IpRange(ModelBase):
4609+
4610+ _columns = {'offset': 'integer', 'length': 'integer'}
4611+ _data_fields = ['offset', 'length', 'policy_id']
4612+
4613+ def contains(self, cidr, address):
4614+ end_index = self.offset + self.length
4615+ end_index_overshoots_length_for_negative_offset = (self.offset < 0
4616+ and end_index >= 0)
4617+ if end_index_overshoots_length_for_negative_offset:
4618+ end_index = None
4619+ return (netaddr.IPAddress(address) in
4620+ netaddr.IPNetwork(cidr)[self.offset:end_index])
4621+
4622+ def _validate(self):
4623+ self._validate_positive_integer('length')
4624+
4625+
4626+class IpOctet(ModelBase):
4627+
4628+ _columns = {'octet': 'integer'}
4629+ _data_fields = ['octet', 'policy_id']
4630+
4631+ @classmethod
4632+ def find_all_by_policy(cls, policy_id):
4633+ return cls.find_all(policy_id=policy_id)
4634+
4635+ def applies_to(self, address):
4636+ return self.octet == netaddr.IPAddress(address).words[-1]
4637+
4638+
4639+class Network(ModelBase):
4640+
4641+ @classmethod
4642+ def find_by(cls, id, tenant_id=None):
4643+ ip_blocks = IpBlock.find_all(network_id=id, tenant_id=tenant_id).all()
4644+ if(len(ip_blocks) == 0):
4645+ raise ModelNotFoundError(_("Network %s not found") % id)
4646+ return cls(id=id, ip_blocks=ip_blocks)
4647+
4648+ @classmethod
4649+ def find_or_create_by(cls, id, tenant_id=None):
4650+ try:
4651+ return cls.find_by(id=id, tenant_id=tenant_id)
4652+ except ModelNotFoundError:
4653+ ip_block = IpBlock.create(cidr=Config.get('default_cidr'),
4654+ network_id=id, tenant_id=tenant_id,
4655+ type="private")
4656+ return cls(id=id, ip_blocks=[ip_block])
4657+
4658+ def allocate_ips(self, addresses=None, **kwargs):
4659+ if addresses:
4660+ return filter(None, [self._allocate_specific_ip(address, **kwargs)
4661+ for address in addresses])
4662+
4663+ ips = [self._allocate_first_free_ip(blocks, **kwargs)
4664+ for blocks in self._block_partitions()]
4665+
4666+ if not any(ips):
4667+ raise NoMoreAddressesError(_("ip blocks in this network are full"))
4668+
4669+ return filter(None, ips)
4670+
4671+ def deallocate_ips(self, interface_id):
4672+ ips = IpAddress.find_all_by_network(self.id, interface_id=interface_id)
4673+ for ip in ips:
4674+ ip.deallocate()
4675+
4676+ def _block_partitions(self):
4677+ return [[block for block in self.ip_blocks
4678+ if not block.is_ipv6()],
4679+ [block for block in self.ip_blocks
4680+ if block.is_ipv6()]]
4681+
4682+ def _allocate_specific_ip(self, address, **kwargs):
4683+ ip_block = utils.find(lambda ip_block: ip_block.contains(address),
4684+ self.ip_blocks)
4685+ if(ip_block is not None):
4686+ try:
4687+ return ip_block.allocate_ip(address=address, **kwargs)
4688+ except DuplicateAddressError:
4689+ pass
4690+
4691+ def _allocate_first_free_ip(self, ip_blocks, **kwargs):
4692+ for ip_block in ip_blocks:
4693+ try:
4694+ return ip_block.allocate_ip(**kwargs)
4695+ except NoMoreAddressesError:
4696+ pass
4697+
4698+
4699+def persisted_models():
4700+ return {'IpBlock': IpBlock, 'IpAddress': IpAddress, 'Policy': Policy,
4701+ 'IpRange': IpRange, 'IpOctet': IpOctet}
4702+
4703+
4704+class NoMoreAddressesError(MelangeError):
4705+
4706+ def _error_message(self):
4707+ return _("no more addresses")
4708+
4709+
4710+class DuplicateAddressError(MelangeError):
4711+
4712+ def _error_message(self):
4713+ return _("Address is already allocated")
4714+
4715+
4716+class AddressDoesNotBelongError(MelangeError):
4717+
4718+ def _error_message(self):
4719+ return _("Address does not belong here")
4720+
4721+
4722+class AddressLockedError(MelangeError):
4723+
4724+ def _error_message(self):
4725+ return _("Address is locked")
4726+
4727+
4728+class ModelNotFoundError(MelangeError):
4729+
4730+ def _error_message(self):
4731+ return _("Not Found")
4732+
4733+
4734+class DataMissingError(MelangeError):
4735+
4736+ def _error_message(self):
4737+ return _("Data Missing")
4738+
4739+
4740+class AddressDisallowedByPolicyError(MelangeError):
4741+
4742+ def _error_message(self):
4743+ return _("Policy does not allow this address")
4744+
4745+
4746+class IpAllocationNotAllowedError(MelangeError):
4747+
4748+ def _error_message(self):
4749+ return _("Ip Block can not allocate address")
4750+
4751+
4752+class InvalidTenantError(MelangeError):
4753+
4754+ def _error_message(self):
4755+ return _("Cannot access other tenant's block")
4756+
4757+
4758+class InvalidModelError(MelangeError):
4759+
4760+ def __init__(self, errors, message=None):
4761+ self.errors = errors
4762+ super(InvalidModelError, self).__init__(message)
4763+
4764+ def __str__(self):
4765+ return _("The following values are invalid: %s") % str(self.errors)
4766+
4767+ def _error_message(self):
4768+ return str(self)
4769+
4770+
4771+def sort(iterable):
4772+ return sorted(iterable, key=lambda model: model.id)
4773
4774=== added file 'melange/ipam/service.py'
4775--- melange/ipam/service.py 1970-01-01 00:00:00 +0000
4776+++ melange/ipam/service.py 2011-08-25 11:33:54 +0000
4777@@ -0,0 +1,426 @@
4778+# vim: tabstop=4 shiftwidth=4 softtabstop=4
4779+
4780+# Copyright 2011 OpenStack LLC.
4781+# All Rights Reserved.
4782+#
4783+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4784+# not use this file except in compliance with the License. You may obtain
4785+# a copy of the License at
4786+#
4787+# http://www.apache.org/licenses/LICENSE-2.0
4788+#
4789+# Unless required by applicable law or agreed to in writing, software
4790+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
4791+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
4792+# License for the specific language governing permissions and limitations
4793+# under the License.
4794+import json
4795+import routes
4796+from webob.exc import HTTPBadRequest
4797+from webob.exc import HTTPConflict
4798+from webob.exc import HTTPNotFound
4799+from webob.exc import HTTPUnprocessableEntity
4800+
4801+from melange.common import wsgi
4802+from melange.common.auth import RoleBasedAuth
4803+from melange.common.config import Config
4804+from melange.common.pagination import PaginatedDataView
4805+from melange.common.pagination import PaginatedResult
4806+from melange.common.utils import exclude
4807+from melange.common.utils import filter_dict
4808+from melange.common.utils import stringify_keys
4809+from melange.common.wsgi import Result
4810+from melange.ipam import models
4811+from melange.ipam.models import IpAddress
4812+from melange.ipam.models import IpBlock
4813+from melange.ipam.models import IpOctet
4814+from melange.ipam.models import IpRange
4815+from melange.ipam.models import Network
4816+from melange.ipam.models import Policy
4817+
4818+
4819+class BaseController(wsgi.Controller):
4820+ exclude_attr = []
4821+ exception_map = {HTTPUnprocessableEntity:
4822+ [models.NoMoreAddressesError,
4823+ models.AddressDoesNotBelongError,
4824+ models.AddressLockedError],
4825+ HTTPBadRequest: [models.InvalidModelError,
4826+ models.DataMissingError],
4827+ HTTPNotFound: [models.ModelNotFoundError],
4828+ HTTPConflict: [models.DuplicateAddressError]}
4829+
4830+ def _extract_required_params(self, request, model_name):
4831+ model_params = request.deserialized_params.get(model_name, {})
4832+ return stringify_keys(exclude(model_params, *self.exclude_attr))
4833+
4834+ def _extract_limits(self, params):
4835+ return dict([(key, params[key]) for key in params.keys()
4836+ if key in ["limit", "marker"]])
4837+
4838+ def _parse_ips(self, addresses):
4839+ return [IpBlock.find_or_allocate_ip(address["ip_block_id"],
4840+ address["ip_address"])
4841+ for address in json.loads(addresses)]
4842+
4843+ def _get_addresses(self, ips):
4844+ return dict(ip_addresses=[ip_address.data() for ip_address in ips])
4845+
4846+ def _paginated_response(self, collection_type, collection_query, request):
4847+ elements, next_marker = collection_query.paginated_collection(
4848+ **self._extract_limits(request.params))
4849+ collection = [element.data() for element in elements]
4850+
4851+ return PaginatedResult(PaginatedDataView(collection_type, collection,
4852+ request.url, next_marker))
4853+
4854+
4855+class IpBlockController(BaseController):
4856+
4857+ exclude_attr = ['tenant_id', 'parent_id']
4858+
4859+ def _find_block(self, **kwargs):
4860+ return IpBlock.find_by(**kwargs)
4861+
4862+ def index(self, request, tenant_id=None):
4863+ type_dict = filter_dict(request.params, 'type')
4864+ all_blocks = IpBlock.find_all(tenant_id=tenant_id, **type_dict)
4865+ return self._paginated_response('ip_blocks', all_blocks, request)
4866+
4867+ def create(self, request, tenant_id=None):
4868+ params = self._extract_required_params(request, 'ip_block')
4869+ block = IpBlock.create(tenant_id=tenant_id, **params)
4870+ return Result(dict(ip_block=block.data()), 201)
4871+
4872+ def update(self, request, id, tenant_id=None):
4873+ ip_block = self._find_block(id=id, tenant_id=tenant_id)
4874+ params = self._extract_required_params(request, 'ip_block')
4875+ ip_block.update(**exclude(params, 'cidr', 'type'))
4876+ return Result(dict(ip_block=ip_block.data()), 200)
4877+
4878+ def show(self, request, id, tenant_id=None):
4879+ ip_block = self._find_block(id=id, tenant_id=tenant_id)
4880+ return dict(ip_block=ip_block.data())
4881+
4882+ def delete(self, request, id, tenant_id=None):
4883+ self._find_block(id=id, tenant_id=tenant_id).delete()
4884+
4885+
4886+class SubnetController(BaseController):
4887+
4888+ def _find_block(self, id, tenant_id):
4889+ return IpBlock.find_by(id=id, tenant_id=tenant_id)
4890+
4891+ def index(self, request, ip_block_id, tenant_id=None):
4892+ ip_block = self._find_block(id=ip_block_id, tenant_id=tenant_id)
4893+ return dict(subnets=[subnet.data() for subnet in ip_block.subnets()])
4894+
4895+ def create(self, request, ip_block_id, tenant_id=None):
4896+ ip_block = self._find_block(id=ip_block_id, tenant_id=tenant_id)
4897+ params = self._extract_required_params(request, 'subnet')
4898+ subnet = ip_block.subnet(**filter_dict(params, 'cidr', 'network_id',
4899+ 'tenant_id'))
4900+ return Result(dict(subnet=subnet.data()), 201)
4901+
4902+
4903+class IpAddressController(BaseController):
4904+
4905+ def _find_block(self, id, tenant_id):
4906+ return IpBlock.find_by(id=id, tenant_id=tenant_id)
4907+
4908+ def index(self, request, ip_block_id, tenant_id=None):
4909+ ip_block = self._find_block(id=ip_block_id, tenant_id=tenant_id)
4910+ addresses = IpAddress.find_all(ip_block_id=ip_block.id)
4911+ return self._paginated_response('ip_addresses', addresses, request)
4912+
4913+ def show(self, request, address, ip_block_id, tenant_id=None):
4914+ ip_block = self._find_block(id=ip_block_id, tenant_id=tenant_id)
4915+ return dict(ip_address=ip_block.find_allocated_ip(address).data())
4916+
4917+ def delete(self, request, address, ip_block_id, tenant_id=None):
4918+ self._find_block(id=ip_block_id,
4919+ tenant_id=tenant_id).deallocate_ip(address)
4920+
4921+ def create(self, request, ip_block_id, tenant_id=None):
4922+ ip_block = self._find_block(id=ip_block_id, tenant_id=tenant_id)
4923+ params = self._extract_required_params(request, 'ip_address')
4924+ params['tenant_id'] = tenant_id or params.get('tenant_id', None)
4925+ ip_address = ip_block.allocate_ip(**params)
4926+ return Result(dict(ip_address=ip_address.data()), 201)
4927+
4928+ def restore(self, request, ip_block_id, address, tenant_id=None):
4929+ ip_address = self._find_block(id=ip_block_id, tenant_id=tenant_id).\
4930+ find_allocated_ip(address)
4931+ ip_address.restore()
4932+
4933+
4934+class InsideGlobalsController(BaseController):
4935+
4936+ def create(self, request, ip_block_id, address):
4937+ local_ip = IpBlock.find_or_allocate_ip(ip_block_id, address)
4938+ global_ips = self._parse_ips(request.params["ip_addresses"])
4939+ local_ip.add_inside_globals(global_ips)
4940+
4941+ def index(self, request, ip_block_id, address):
4942+ ip = IpBlock.find(ip_block_id).find_allocated_ip(address)
4943+ return self._get_addresses(ip.inside_globals(
4944+ **self._extract_limits(request.params)))
4945+
4946+ def delete(self, request, ip_block_id, address,
4947+ inside_globals_address=None):
4948+ local_ip = IpBlock.find(ip_block_id).find_allocated_ip(address)
4949+ local_ip.remove_inside_globals(inside_globals_address)
4950+
4951+
4952+class InsideLocalsController(BaseController):
4953+
4954+ def create(self, request, ip_block_id, address):
4955+ global_ip = IpBlock.find_or_allocate_ip(ip_block_id, address)
4956+ local_ips = self._parse_ips(request.params["ip_addresses"])
4957+ global_ip.add_inside_locals(local_ips)
4958+
4959+ def index(self, request, ip_block_id, address):
4960+ ip = IpBlock.find(ip_block_id).find_allocated_ip(address)
4961+ return self._get_addresses(ip.inside_locals(
4962+ **self._extract_limits(request.params)))
4963+
4964+ def delete(self, request, ip_block_id, address,
4965+ inside_locals_address=None):
4966+ global_ip = IpBlock.find(ip_block_id).find_allocated_ip(address)
4967+ global_ip.remove_inside_locals(inside_locals_address)
4968+
4969+
4970+class UnusableIpRangesController(BaseController):
4971+
4972+ def create(self, request, policy_id, tenant_id=None):
4973+ policy = Policy.find_by(id=policy_id, tenant_id=tenant_id)
4974+ params = self._extract_required_params(request, 'ip_range')
4975+ ip_range = policy.create_unusable_range(**params)
4976+ return Result(dict(ip_range=ip_range.data()), 201)
4977+
4978+ def show(self, request, policy_id, id, tenant_id=None):
4979+ ip_range = Policy.find_by(id=policy_id,
4980+ tenant_id=tenant_id).find_ip_range(id)
4981+ return dict(ip_range=ip_range.data())
4982+
4983+ def index(self, request, policy_id, tenant_id=None):
4984+ policy = Policy.find_by(id=policy_id,
4985+ tenant_id=tenant_id)
4986+ ip_ranges = IpRange.find_all(policy_id=policy.id)
4987+ return self._paginated_response('ip_ranges', ip_ranges, request)
4988+
4989+ def update(self, request, policy_id, id, tenant_id=None):
4990+ ip_range = Policy.find_by(id=policy_id,
4991+ tenant_id=tenant_id).find_ip_range(id)
4992+ params = self._extract_required_params(request, 'ip_range')
4993+ ip_range.update(**exclude(params, 'policy_id'))
4994+ return dict(ip_range=ip_range.data())
4995+
4996+ def delete(self, request, policy_id, id, tenant_id=None):
4997+ ip_range = Policy.find_by(id=policy_id,
4998+ tenant_id=tenant_id).find_ip_range(id)
4999+ ip_range.delete()
5000+
The diff has been truncated for viewing.