Merge lp:~raxnetworking/nova/melange into lp:~hudson-openstack/nova/trunk
- melange
- Merge into trunk
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 |
Related bugs: | |
Related blueprints: |
Melange - API
(Undefined)
Melange - IP Address Management Service
(Undefined)
|
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.
Commit message
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.
- 1339. By Trey Morris
-
moved double author to .mailmap
Josh Kearney (jk0) wrote : | # |
> Getting some errors http://
> (are you running novaclient 2.6.1?)
That's odd. All tests pass for me running the latest novaclient.
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_
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
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
- 1340. By Trey Morris
-
alphabetized melange imports
- 1341. By Trey Morris
-
correct copyright
Sandy Walsh (sandy-walsh) wrote : | # |
unit tests pass, but functional tests won't run
http://
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
- 1342. By Trey Morris
-
fixed extra l in sqlite
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?
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.
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?
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-
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/
9) Nobody is upgrading melange yet, so not sure any of the db-migrate scripts are necessary
Cheers,
jay
Sandy Walsh (sandy-walsh) wrote : | # |
+1 to Jay's feedback.
This should really be our motivation to push on openstack-common.
Jay Pipes (jaypipes) wrote : | # |
Where we are putting common code (that will eventually become openstack.
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.
> 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.
possible.
> === added file 'melange/
>
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).
Sandy Walsh (sandy-walsh) wrote : | # |
+1 to Rick's suggestion of, at least, fixing the imports to use nova's existing code base.
- 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.MethodIns
pector. 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
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 | + |
Getting some errors http:// paste.openstack .org/show/ 2187/
(are you running novaclient 2.6.1?)