summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Garbutt <john.garbutt@citrix.com>2012-01-13 13:36:01 +0000
committerArmando Migliaccio <armando.migliaccio@eu.citrix.com>2012-01-26 19:01:02 +0000
commit0213eb4c0a364759e9de2fa25f46b43235db94bb (patch)
tree6a8b27339729827f11dc347f74fc2a3457d6a571
parent36be4bf5759ae1f22c6eeeff5be01cf20e068bf3 (diff)
downloadpython-novaclient-0213eb4c0a364759e9de2fa25f46b43235db94bb.tar.gz
blueprint host-aggregates: client bindings
These are the client bindings for the new aggregate api added into nova in the following change: https://review.openstack.org/#change,3109 Change-Id: I97e0223aa18d01450f82848c5be9bce78b83ef39
-rw-r--r--AUTHORS1
-rw-r--r--README.rst12
-rw-r--r--novaclient/base.py1
-rw-r--r--novaclient/v1_1/aggregates.py89
-rw-r--r--novaclient/v1_1/client.py2
-rw-r--r--novaclient/v1_1/shell.py112
-rw-r--r--tests/v1_1/fakes.py38
-rw-r--r--tests/v1_1/test_aggregates.py129
-rw-r--r--tests/v1_1/test_shell.py44
9 files changed, 411 insertions, 17 deletions
diff --git a/AUTHORS b/AUTHORS
index f84d6dbe..67977533 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -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>
diff --git a/README.rst b/README.rst
index 860672b6..d8049793 100644
--- a/README.rst
+++ b/README.rst
@@ -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')