diff options
-rw-r--r-- | AUTHORS | 1 | ||||
-rw-r--r-- | README.rst | 12 | ||||
-rw-r--r-- | novaclient/base.py | 1 | ||||
-rw-r--r-- | novaclient/v1_1/aggregates.py | 89 | ||||
-rw-r--r-- | novaclient/v1_1/client.py | 2 | ||||
-rw-r--r-- | novaclient/v1_1/shell.py | 112 | ||||
-rw-r--r-- | tests/v1_1/fakes.py | 38 | ||||
-rw-r--r-- | tests/v1_1/test_aggregates.py | 129 | ||||
-rw-r--r-- | tests/v1_1/test_shell.py | 44 |
9 files changed, 411 insertions, 17 deletions
@@ -24,6 +24,7 @@ Jason Kölker <jason@koelker.net> Jason Straw <jason.straw@rackspace.com> Jesse Andrews <anotherjesse@gmail.com> Johannes Erdfelt <johannes.erdfelt@rackspace.com> +John Garbutt <john.garbutt@citrix.com> Josh Kearney <josh@jk0.org> Kevin L. Mitchell <kevin.mitchell@rackspace.com> Kiall Mac Innes <kiall@managedit.ie> @@ -77,6 +77,18 @@ You'll find complete documentation on the shell by running <subcommand> add-fixed-ip Add a new fixed IP address to a servers network. add-floating-ip Add a floating IP address to a server. + aggregate-add-host Add the host to the specified aggregate + aggregate-create Create a new aggregate with the specified details + aggregate-delete Delete the aggregate by its id + aggregate-details Show details of the specified aggregate + aggregate-list Print a list of all aggregates + aggregate-remove-host + Remove the specified host from the specfied + aggregate + aggregate-set-metadata + Update the metadata associated with the aggregate + aggregate-update Update the aggregate's name and optionally + availablity zone backup Backup a server. backup-schedule Show or edit the backup schedule for a server. backup-schedule-delete diff --git a/novaclient/base.py b/novaclient/base.py index 94e82508..5894e076 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -141,6 +141,7 @@ class Manager(utils.HookableMixin): def _update(self, url, body, **kwargs): self.run_hooks('modify_body_for_update', body, **kwargs) resp, body = self.api.client.put(url, body=body) + return body class ManagerWithFind(Manager): diff --git a/novaclient/v1_1/aggregates.py b/novaclient/v1_1/aggregates.py new file mode 100644 index 00000000..be9421e9 --- /dev/null +++ b/novaclient/v1_1/aggregates.py @@ -0,0 +1,89 @@ +# Copyright 2012 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Aggregate interface.""" + +from novaclient import base + + +class Aggregate(base.Resource): + """An aggregates is a collection of compute hosts.""" + + def __repr__(self): + return "<Aggregate: %s>" % self.id + + def update(self, values): + """Update the name and/or availability zone.""" + return self.manager.update(self, values) + + def add_host(self, host): + return self.manager.add_host(self, host) + + def remove_host(self, host): + return self.manager.remove_host(self, host) + + def set_metadata(self, metadata): + return self.manager.set_metadata(self, metadata) + + def delete(self): + self.manager.delete(self) + + +class AggregateManager(base.ManagerWithFind): + resource_class = Aggregate + + def list(self): + """Get a list of os-aggregates.""" + return self._list('/os-aggregates', 'aggregates') + + def create(self, name, availability_zone): + """Create a new aggregate.""" + body = {'aggregate': {'name': name, + 'availability_zone': availability_zone}} + return self._create('/os-aggregates', body, 'aggregate') + + def get_details(self, aggregate): + """Get details of the specified aggregate.""" + return self._get('/os-aggregates/%s' % (base.getid(aggregate)), + "aggregate") + + def update(self, aggregate, values): + """Update the name and/or availablity zone.""" + body = {'aggregate': values} + result = self._update("/os-aggregates/%s" % base.getid(aggregate), + body) + return self.resource_class(self, result["aggregate"]) + + def add_host(self, aggregate, host): + """Add a host into the Host Aggregate.""" + body = {'add_host': {'host': host}} + return self._create("/os-aggregates/%s/action" % base.getid(aggregate), + body, "aggregate") + + def remove_host(self, aggregate, host): + """Remove a host from the Host Aggregate.""" + body = {'remove_host': {'host': host}} + return self._create("/os-aggregates/%s/action" % base.getid(aggregate), + body, "aggregate") + + def set_metadata(self, aggregate, metadata): + """Set a aggregate metadata, replacing the existing metadata.""" + body = {'set_metadata': {'metadata': metadata}} + return self._create("/os-aggregates/%s/action" % base.getid(aggregate), + body, "aggregate") + + def delete(self, aggregate): + """Delete the specified aggregates.""" + self._delete('/os-aggregates/%s' % (base.getid(aggregate))) diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 96576f9b..89fcdb52 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -1,5 +1,6 @@ from novaclient import client from novaclient.v1_1 import certs +from novaclient.v1_1 import aggregates from novaclient.v1_1 import flavors from novaclient.v1_1 import floating_ip_dns from novaclient.v1_1 import floating_ips @@ -64,6 +65,7 @@ class Client(object): self.usage = usage.UsageManager(self) self.virtual_interfaces = \ virtual_interfaces.VirtualInterfaceManager(self) + self.aggregates = aggregates.AggregateManager(self) # Add in any extensions... if extensions: diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index b69c9e5f..84b40e49 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -380,6 +380,15 @@ def do_image_list(cs, args): def do_image_meta(cs, args): """Set or Delete metadata on an image.""" image = _find_image(cs, args.image) + metadata = _extract_metadata(args) + + if args.action == 'set': + cs.images.set_meta(image, metadata) + elif args.action == 'delete': + cs.images.delete_meta(image, metadata.keys()) + + +def _extract_metadata(args): metadata = {} for metadatum in args.metadata[0]: # Can only pass the key in on 'delete' @@ -391,11 +400,7 @@ def do_image_meta(cs, args): value = None metadata[key] = value - - if args.action == 'set': - cs.images.set_meta(image, metadata) - elif args.action == 'delete': - cs.images.delete_meta(image, metadata.keys()) + return metadata def _print_image(image): @@ -679,17 +684,7 @@ def do_image_create(cs, args): def do_meta(cs, args): """Set or Delete metadata on a server.""" server = _find_server(cs, args.server) - metadata = {} - for metadatum in args.metadata[0]: - # Can only pass the key in on 'delete' - # So this doesn't have to have '=' - if metadatum.find('=') > -1: - (key, value) = metadatum.split('=', 1) - else: - key = metadatum - value = None - - metadata[key] = value + metadata = _extract_metadata(args) if args.action == 'set': cs.servers.set_meta(server, metadata) @@ -1298,7 +1293,7 @@ def do_secgroup_delete_group_rule(cs, args): if (rule.get('ip_protocol') == params.get('ip_protocol') and rule.get('from_port') == params.get('from_port') and rule.get('to_port') == params.get('to_port') and - rule.get('group', {}).get('name') ==\ + rule.get('group', {}).get('name') == \ params.get('group_name')): return cs.security_group_rules.delete(rule['id']) @@ -1445,3 +1440,86 @@ def do_x509_get_root_cert(cs, args): cacert = cs.certs.get() cert.write(cacert.data) print "Wrote x509 root cert to %s" % args.filename + + +def do_aggregate_list(cs, args): + """Print a list of all aggregates.""" + aggregates = cs.aggregates.list() + columns = ['Id', 'Name', 'Availability Zone', 'Operational State'] + utils.print_list(aggregates, columns) + + +@utils.arg('name', metavar='<name>', help='Name of aggregate.') +@utils.arg('availability_zone', metavar='<availability_zone>', + help='The availablity zone of the aggregate.') +def do_aggregate_create(cs, args): + """Create a new aggregate with the specified details.""" + aggregate = cs.aggregates.create(args.name, args.availability_zone) + _print_aggregate_details(aggregate) + + +@utils.arg('id', metavar='<id>', help='Aggregate id to delete.') +def do_aggregate_delete(cs, args): + """Delete the aggregate by its id.""" + cs.aggregates.delete(args.id) + print "Aggregate %s has been succesfully deleted." % args.id + + +@utils.arg('id', metavar='<id>', help='Aggregate id to udpate.') +@utils.arg('name', metavar='<name>', help='Name of aggregate.') +@utils.arg('availability_zone', metavar='<availability_zone>', + help='The availablity zone of the aggregate.', nargs='?') +def do_aggregate_update(cs, args): + """Update the aggregate's name and optionally availablity zone.""" + updates = {"name": args.name} + if args.availability_zone: + updates["availability_zone"] = args.availability_zone + + aggregate = cs.aggregates.update(args.id, updates) + print "Aggregate %s has been succesfully updated." % args.id + _print_aggregate_details(aggregate) + + +@utils.arg('id', metavar='<id>', help='Aggregate id to udpate.') +@utils.arg('metadata', + metavar='<key=value>', + nargs='+', + action='append', + default=[], + help='Metadata to add/update to aggregate') +def do_aggregate_set_metadata(cs, args): + """Update the metadata associated with the aggregate.""" + metadata = _extract_metadata(args) + aggregate = cs.aggregates.set_metadata(args.id, metadata) + print "Aggregate %s has been succesfully updated." % args.id + _print_aggregate_details(aggregate) + + +@utils.arg('id', metavar='<id>', help='Host aggregate id to delete.') +@utils.arg('host', metavar='<host>', help='The host to add to the aggregate.') +def do_aggregate_add_host(cs, args): + """Add the host to the specified aggregate.""" + aggregate = cs.aggregates.add_host(args.id, args.host) + print "Aggregate %s has been succesfully updated." % args.id + _print_aggregate_details(aggregate) + + +@utils.arg('id', metavar='<id>', help='Host aggregate id to delete.') +@utils.arg('host', metavar='<host>', help='The host to add to the aggregate.') +def do_aggregate_remove_host(cs, args): + """Remove the specified host from the specfied aggregate.""" + aggregate = cs.aggregates.remove_host(args.id, args.host) + print "Aggregate %s has been succesfully updated." % args.id + _print_aggregate_details(aggregate) + + +@utils.arg('id', metavar='<id>', help='Host aggregate id to delete.') +def do_aggregate_details(cs, args): + """Show details of the specified aggregate.""" + _print_aggregate_details(cs.aggregates.get_details(args.id)) + + +def _print_aggregate_details(aggregate): + columns = ['Id', 'Name', 'Availability Zone', 'Operational State', + 'Hosts', 'Metadata'] + utils.print_list([aggregate], columns) diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 1cfe4668..045b5c35 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -708,3 +708,41 @@ class FakeHTTPClient(base_client.HTTPClient): def post_os_certificates(self, **kw): return (200, {'certificate': {'private_key': 'foo', 'data': 'bar'}}) + + # + # Aggregates + # + def get_os_aggregates(self, *kw): + return (200, {"aggregates": [ + {'id':'1', + 'name': 'test', + 'availability_zone': 'nova1'}, + {'id':'2', + 'name': 'test2', + 'availability_zone': 'nova1'}, + ]}) + + def _return_aggregate(self): + r = {'aggregate': self.get_os_aggregates()[1]['aggregates'][0]} + return (200, r) + + def get_os_aggregates_1(self, **kw): + return self._return_aggregate() + + def post_os_aggregates(self, body, **kw): + return self._return_aggregate() + + def put_os_aggregates_1(self, body, **kw): + return self._return_aggregate() + + def put_os_aggregates_2(self, body, **kw): + return self._return_aggregate() + + def post_os_aggregates_1_action(self, body, **kw): + return self._return_aggregate() + + def post_os_aggregates_2_action(self, body, **kw): + return self._return_aggregate() + + def delete_os_aggregates_1(self, **kw): + return (202, None) diff --git a/tests/v1_1/test_aggregates.py b/tests/v1_1/test_aggregates.py new file mode 100644 index 00000000..d5ea1bbb --- /dev/null +++ b/tests/v1_1/test_aggregates.py @@ -0,0 +1,129 @@ +# Copyright 2012 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.v1_1 import aggregates +from tests import utils +from tests.v1_1 import fakes + + +cs = fakes.FakeClient() + + +class AggregatesTest(utils.TestCase): + + def test_list_aggregates(self): + result = cs.aggregates.list() + cs.assert_called('GET', '/os-aggregates') + for aggregate in result: + self.assertTrue(isinstance(aggregate, aggregates.Aggregate)) + + def test_create_aggregate(self): + body = {"aggregate": {"name": "test", "availability_zone": "nova1"}} + aggregate = cs.aggregates.create("test", "nova1") + cs.assert_called('POST', '/os-aggregates', body) + self.assertTrue(isinstance(aggregate, aggregates.Aggregate)) + + def test_get_details(self): + aggregate = cs.aggregates.get_details("1") + cs.assert_called('GET', '/os-aggregates/1') + self.assertTrue(isinstance(aggregate, aggregates.Aggregate)) + + aggregate2 = cs.aggregates.get_details(aggregate) + cs.assert_called('GET', '/os-aggregates/1') + self.assertTrue(isinstance(aggregate2, aggregates.Aggregate)) + + def test_update(self): + aggregate = cs.aggregates.get_details("1") + values = {"name": "foo"} + body = {"aggregate": values} + + result1 = aggregate.update(values) + cs.assert_called('PUT', '/os-aggregates/1', body) + self.assertTrue(isinstance(result1, aggregates.Aggregate)) + + result2 = cs.aggregates.update(2, values) + cs.assert_called('PUT', '/os-aggregates/2', body) + self.assertTrue(isinstance(result2, aggregates.Aggregate)) + + def test_update_with_availablity_zone(self): + aggregate = cs.aggregates.get_details("1") + values = {"name": "foo", "availability_zone": "new_zone"} + body = {"aggregate": values} + + result3 = cs.aggregates.update(aggregate, values) + cs.assert_called('PUT', '/os-aggregates/1', body) + self.assertTrue(isinstance(result3, aggregates.Aggregate)) + + def test_add_host(self): + aggregate = cs.aggregates.get_details("1") + host = "host1" + body = {"add_host": {"host": "host1"}} + + result1 = aggregate.add_host(host) + cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assertTrue(isinstance(result1, aggregates.Aggregate)) + + result2 = cs.aggregates.add_host("2", host) + cs.assert_called('POST', '/os-aggregates/2/action', body) + self.assertTrue(isinstance(result2, aggregates.Aggregate)) + + result3 = cs.aggregates.add_host(aggregate, host) + cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assertTrue(isinstance(result3, aggregates.Aggregate)) + + def test_remove_host(self): + aggregate = cs.aggregates.get_details("1") + host = "host1" + body = {"remove_host": {"host": "host1"}} + + result1 = aggregate.remove_host(host) + cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assertTrue(isinstance(result1, aggregates.Aggregate)) + + result2 = cs.aggregates.remove_host("2", host) + cs.assert_called('POST', '/os-aggregates/2/action', body) + self.assertTrue(isinstance(result2, aggregates.Aggregate)) + + result3 = cs.aggregates.remove_host(aggregate, host) + cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assertTrue(isinstance(result3, aggregates.Aggregate)) + + def test_set_metadata(self): + aggregate = cs.aggregates.get_details("1") + metadata = {"foo": "bar"} + body = {"set_metadata": {"metadata": metadata}} + + result1 = aggregate.set_metadata(metadata) + cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assertTrue(isinstance(result1, aggregates.Aggregate)) + + result2 = cs.aggregates.set_metadata(2, metadata) + cs.assert_called('POST', '/os-aggregates/2/action', body) + self.assertTrue(isinstance(result2, aggregates.Aggregate)) + + result3 = cs.aggregates.set_metadata(aggregate, metadata) + cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assertTrue(isinstance(result3, aggregates.Aggregate)) + + def test_delete_aggregate(self): + aggregate = cs.aggregates.list()[0] + aggregate.delete() + cs.assert_called('DELETE', '/os-aggregates/1') + + cs.aggregates.delete('1') + cs.assert_called('DELETE', '/os-aggregates/1') + + cs.aggregates.delete(aggregate) + cs.assert_called('DELETE', '/os-aggregates/1') diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index c0db8da7..c7d0d0d3 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -387,3 +387,47 @@ class ShellTest(utils.TestCase): self.assert_called('POST', '/flavors', body, pos=-2) self.assert_called('GET', '/flavors/1') + + def test_aggregate_list(self): + self.run_command('aggregate-list') + self.assert_called('GET', '/os-aggregates') + + def test_aggregate_create(self): + self.run_command('aggregate-create test_name nova1') + body = {"aggregate": {"name": "test_name", + "availability_zone": "nova1"}} + self.assert_called('POST', '/os-aggregates', body) + + def test_aggregate_delete(self): + self.run_command('aggregate-delete 1') + self.assert_called('DELETE', '/os-aggregates/1') + + def test_aggregate_update(self): + self.run_command('aggregate-update 1 new_name') + body = {"aggregate": {"name": "new_name"}} + self.assert_called('PUT', '/os-aggregates/1', body) + + def test_aggregate_update_with_availablity_zone(self): + self.run_command('aggregate-update 1 foo new_zone') + body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} + self.assert_called('PUT', '/os-aggregates/1', body) + + def test_aggregate_set_metadata(self): + self.run_command('aggregate-set-metadata 1 foo=bar delete_key') + body = {"set_metadata": {"metadata": {"foo": "bar", + "delete_key": None}}} + self.assert_called('POST', '/os-aggregates/1/action', body) + + def test_aggregate_add_host(self): + self.run_command('aggregate-add-host 1 host1') + body = {"add_host": {"host": "host1"}} + self.assert_called('POST', '/os-aggregates/1/action', body) + + def test_aggregate_remove_host(self): + self.run_command('aggregate-remove-host 1 host1') + body = {"remove_host": {"host": "host1"}} + self.assert_called('POST', '/os-aggregates/1/action', body) + + def test_aggregate_details(self): + self.run_command('aggregate-details 1') + self.assert_called('GET', '/os-aggregates/1') |