Use Resource layer for next compute methods

- server_groups
- server_console
- create_image
- server_metadata

Change-Id: I26f3a22bf9d9e397c2a032cf2b66ec60d8040b51
This commit is contained in:
Artem Goncharov 2019-06-07 08:48:31 +02:00
parent f7860861a1
commit 9cce631094
10 changed files with 235 additions and 95 deletions

View File

@ -363,7 +363,8 @@ class ComputeCloudMixin(_normalize.Normalizer):
# and the to_munch call.
self._normalize_server(server._to_munch())
for server in self.compute.servers(
all_projects=all_projects, **filters)]
all_projects=all_projects, allow_unknown_params=True,
**filters)]
return [
self._expand_server(server, detailed, bare)
for server in servers
@ -375,10 +376,7 @@ class ComputeCloudMixin(_normalize.Normalizer):
:returns: A list of server group dicts.
"""
data = proxy._json_response(
self.compute.get('/os-server-groups'),
error_message="Error fetching server group list")
return self._get_and_munchify('server_groups', data)
return list(self.compute.server_groups())
def get_compute_limits(self, name_or_id=None):
""" Get compute limits for a project
@ -521,10 +519,12 @@ class ComputeCloudMixin(_normalize.Normalizer):
return ""
def _get_server_console_output(self, server_id, length=None):
data = proxy._json_response(self.compute.post(
'/servers/{server_id}/action'.format(server_id=server_id),
json={'os-getConsoleOutput': {'length': length}}))
return self._get_and_munchify('output', data)
output = self.compute.get_server_console_output(
server=server_id,
length=length
)
if 'output' in output:
return output['output']
def get_server(
self, name_or_id=None, filters=None, detailed=False, bare=False,
@ -678,40 +678,9 @@ class ComputeCloudMixin(_normalize.Normalizer):
"Server {server} could not be found and therefore"
" could not be snapshotted.".format(server=server))
server = server_obj
response = proxy._json_response(
self.compute.post(
'/servers/{server_id}/action'.format(server_id=server['id']),
json={
"createImage": {
"name": name,
"metadata": metadata,
}
}))
# You won't believe it - wait, who am I kidding - of course you will!
# Nova returns the URL of the image created in the Location
# header of the response. (what?) But, even better, the URL it responds
# with has a very good chance of being wrong (it is built from
# nova.conf values that point to internal API servers in any cloud
# large enough to have both public and internal endpoints.
# However, nobody has ever noticed this because novaclient doesn't
# actually use that URL - it extracts the id from the end of
# the url, then returns the id. This leads us to question:
# a) why Nova is going to return a value in a header
# b) why it's going to return data that probably broken
# c) indeed the very nature of the fabric of reality
# Although it fills us with existential dread, we have no choice but
# to follow suit like a lemming being forced over a cliff by evil
# producers from Disney.
# TODO(mordred) Update this to consume json microversion when it is
# available.
# blueprint:remove-create-image-location-header-response
image_id = response.headers['Location'].rsplit('/', 1)[1]
self.list_images.invalidate(self)
image = self.get_image(image_id)
if not wait:
return image
return self.wait_for_image(image, timeout=timeout)
image = self.compute.create_server_image(
server, name=name, metadata=metadata, wait=wait, timeout=timeout)
return image
def get_server_id(self, name_or_id):
server = self.get_server(name_or_id, bare=True)
@ -1100,6 +1069,9 @@ class ComputeCloudMixin(_normalize.Normalizer):
"""
Wait for a server to reach ACTIVE status.
"""
# server = self.compute.wait_for_server(
# server=server, interval=self._SERVER_AGE or 2, wait=timeout
# )
server_id = server['id']
timeout_message = "Timeout waiting for the server to come up."
start_time = time.time()
@ -1238,11 +1210,7 @@ class ComputeCloudMixin(_normalize.Normalizer):
raise exc.OpenStackCloudException(
'Invalid Server {server}'.format(server=name_or_id))
proxy._json_response(
self.compute.post(
'/servers/{server_id}/metadata'.format(server_id=server['id']),
json={'metadata': metadata}),
error_message='Error updating server metadata')
self.compute.set_server_metadata(server=server.id, **metadata)
def delete_server_metadata(self, name_or_id, metadata_keys):
"""Delete metadata from a server instance.
@ -1259,15 +1227,8 @@ class ComputeCloudMixin(_normalize.Normalizer):
raise exc.OpenStackCloudException(
'Invalid Server {server}'.format(server=name_or_id))
for key in metadata_keys:
error_message = 'Error deleting metadata {key} on {server}'.format(
key=key, server=name_or_id)
proxy._json_response(
self.compute.delete(
'/servers/{server_id}/metadata/{key}'.format(
server_id=server['id'],
key=key)),
error_message=error_message)
self.compute.delete_server_metadata(server=server.id,
keys=metadata_keys)
def delete_server(
self, name_or_id, wait=False, timeout=180, delete_ips=False,
@ -1410,7 +1371,7 @@ class ComputeCloudMixin(_normalize.Normalizer):
self._get_and_munchify('server', data))
return self._expand_server(server, bare=bare, detailed=detailed)
def create_server_group(self, name, policies):
def create_server_group(self, name, policies=[], policy=None):
"""Create a new server group.
:param name: Name of the server group being created
@ -1420,16 +1381,16 @@ class ComputeCloudMixin(_normalize.Normalizer):
:raises: OpenStackCloudException on operation error.
"""
data = proxy._json_response(
self.compute.post(
'/os-server-groups',
json={
'server_group': {
'name': name,
'policies': policies}}),
error_message="Unable to create server group {name}".format(
name=name))
return self._get_and_munchify('server_group', data)
sg_attrs = {
'name': name
}
if policies:
sg_attrs['policies'] = policies
if policy:
sg_attrs['policy'] = policy
return self.compute.create_server_group(
**sg_attrs
)
def delete_server_group(self, name_or_id):
"""Delete a server group.
@ -1446,12 +1407,7 @@ class ComputeCloudMixin(_normalize.Normalizer):
name_or_id)
return False
proxy._json_response(
self.compute.delete(
'/os-server-groups/{id}'.format(id=server_group['id'])),
error_message="Error deleting server group {name}".format(
name=name_or_id))
self.compute.delete_server_group(server_group, ignore_missing=False)
return True
def create_flavor(self, name, ram, vcpus, disk, flavorid="auto",

View File

@ -623,18 +623,26 @@ class Proxy(proxy.Proxy):
server = self._get_resource(_server.Server, server)
server.revert_resize(self)
def create_server_image(self, server, name, metadata=None):
def create_server_image(self, server, name, metadata=None, wait=False,
timeout=120):
"""Create an image from a server
:param server: Either the ID of a server or a
:class:`~openstack.compute.v2.server.Server` instance.
:class:`~openstack.compute.v2.server.Server` instance.
:param str name: The name of the image to be created.
:param dict metadata: A dictionary of metadata to be set on the image.
:returns: None
:returns: :class:`~openstack.image.v2.image.Image` object.
"""
server = self._get_resource(_server.Server, server)
server.create_image(self, name, metadata)
image_id = server.create_image(self, name, metadata)
self._connection.list_images.invalidate(self)
image = self._connection.get_image(image_id)
if not wait:
return image
return self._connection.wait_for_image(image, timeout=timeout)
def add_security_group_to_server(self, server, security_group):
"""Add a security group to a server

View File

@ -12,6 +12,7 @@
from openstack.compute.v2 import metadata
from openstack.image.v2 import image
from openstack import exceptions
from openstack import resource
from openstack import utils
@ -233,8 +234,10 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin):
# the URL used is sans any additional /detail/ part.
url = utils.urljoin(Server.base_path, self.id, 'action')
headers = {'Accept': ''}
return session.post(
response = session.post(
url, json=body, headers=headers, microversion=microversion)
exceptions.raise_from_response(response)
return response
def change_password(self, session, new_password):
"""Change the administrator password to the given password."""
@ -303,7 +306,39 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin):
if metadata is not None:
action['metadata'] = metadata
body = {'createImage': action}
self._action(session, body)
# You won't believe it - wait, who am I kidding - of course you will!
# Nova returns the URL of the image created in the Location
# header of the response. (what?) But, even better, the URL it responds
# with has a very good chance of being wrong (it is built from
# nova.conf values that point to internal API servers in any cloud
# large enough to have both public and internal endpoints.
# However, nobody has ever noticed this because novaclient doesn't
# actually use that URL - it extracts the id from the end of
# the url, then returns the id. This leads us to question:
# a) why Nova is going to return a value in a header
# b) why it's going to return data that probably broken
# c) indeed the very nature of the fabric of reality
# Although it fills us with existential dread, we have no choice but
# to follow suit like a lemming being forced over a cliff by evil
# producers from Disney.
microversion = None
if utils.supports_microversion(session, '2.45'):
microversion = '2.45'
response = self._action(session, body, microversion)
body = None
try:
# There might be body, might be not
body = response.json()
except Exception:
pass
if body and 'image_id' in body:
image_id = body['image_id']
else:
image_id = response.headers['Location'].rsplit('/', 1)[1]
return image_id
def add_security_group(self, session, security_group):
body = {"addSecurityGroup": {"name": security_group}}

View File

@ -10,7 +10,9 @@
# License for the specific language governing permissions and limitations
# under the License.
from openstack import exceptions
from openstack import resource
from openstack import utils
class ServerGroup(resource.Resource):
@ -20,6 +22,8 @@ class ServerGroup(resource.Resource):
_query_mapping = resource.QueryParameters("all_projects")
_max_microversion = '2.64'
# capabilities
allow_create = True
allow_fetch = True
@ -29,9 +33,53 @@ class ServerGroup(resource.Resource):
# Properties
#: A name identifying the server group
name = resource.Body('name')
#: The list of policies supported by the server group
#: The list of policies supported by the server group (till 2.63)
policies = resource.Body('policies')
#: The policy field represents the name of the policy (from 2.64)
policy = resource.Body('policy')
#: The list of members in the server group
member_ids = resource.Body('members')
#: The metadata associated with the server group
metadata = resource.Body('metadata')
#: The project ID who owns the server group.
project_id = resource.Body('project_id')
#: The rules field, which is a dict, can be applied to the policy
rules = resource.Body('rules', type=list, list_type=dict)
#: The user ID who owns the server group
user_id = resource.Body('user_id')
def _get_microversion_for(self, session, action):
"""Get microversion to use for the given action.
The base version uses :meth:`_get_microversion_for_list`.
Subclasses can override this method if more complex logic is needed.
:param session: :class`keystoneauth1.adapter.Adapter`
:param action: One of "fetch", "commit", "create", "delete", "patch".
Unused in the base implementation.
:return: microversion as string or ``None``
"""
if action not in ('fetch', 'commit', 'create', 'delete', 'patch'):
raise ValueError('Invalid action: %s' % action)
microversion = self._get_microversion_for_list(session)
if action == 'create':
# `policy` and `rules` are added with mv=2.64. In it also
# `policies` are removed.
if utils.supports_microversion(session, '2.64'):
if self.policies:
if not self.policy and isinstance(self.policies, list):
self.policy = self.policies[0]
self.policies = None
microversion = self._max_microversion
else:
if self.rules:
message = ("API version %s is required to set rules, but "
"it is not available.") % 2.64
raise exceptions.NotSupported(message)
if self.policy:
if not self.policies:
self.policies = [self.policy]
self.policy = None
return microversion

View File

@ -33,6 +33,7 @@ class TestImageSnapshot(base.TestCase):
snapshot_name = 'test-snapshot'
fake_image = fakes.make_fake_image(self.image_id, status='pending')
self.register_uris([
self.get_nova_discovery_mock_dict(),
dict(
method='POST',
uri='{endpoint}/servers/{server_id}/action'.format(
@ -70,6 +71,7 @@ class TestImageSnapshot(base.TestCase):
pending_image = fakes.make_fake_image(self.image_id, status='pending')
fake_image = fakes.make_fake_image(self.image_id)
self.register_uris([
self.get_nova_discovery_mock_dict(),
dict(
method='POST',
uri='{endpoint}/servers/{server_id}/action'.format(

View File

@ -36,11 +36,11 @@ class TestServerConsole(base.TestCase):
id=self.server_id),
json={"output": self.output},
validate=dict(
json={'os-getConsoleOutput': {'length': None}}))
json={'os-getConsoleOutput': {'length': 5}}))
])
self.assertEqual(
self.output, self.cloud.get_server_console(self.server))
self.output, self.cloud.get_server_console(self.server, 5))
self.assert_calls()
def test_get_server_console_name_or_id(self):
@ -57,7 +57,7 @@ class TestServerConsole(base.TestCase):
id=self.server_id),
json={"output": self.output},
validate=dict(
json={'os-getConsoleOutput': {'length': None}}))
json={'os-getConsoleOutput': {}}))
])
self.assertEqual(
@ -74,7 +74,7 @@ class TestServerConsole(base.TestCase):
id=self.server_id),
status_code=400,
validate=dict(
json={'os-getConsoleOutput': {'length': None}}))
json={'os-getConsoleOutput': {}}))
])
self.assertEqual('', self.cloud.get_server_console(self.server))

View File

@ -30,6 +30,7 @@ class TestServerGroup(base.TestCase):
def test_create_server_group(self):
self.register_uris([
self.get_nova_discovery_mock_dict(),
dict(method='POST',
uri=self.get_mock_url(
'compute', 'public', append=['os-server-groups']),
@ -48,6 +49,7 @@ class TestServerGroup(base.TestCase):
def test_delete_server_group(self):
self.register_uris([
self.get_nova_discovery_mock_dict(),
dict(method='GET',
uri=self.get_mock_url(
'compute', 'public', append=['os-server-groups']),

View File

@ -57,6 +57,7 @@ class TestServerSetMetadata(base.TestCase):
self.assert_calls()
def test_server_set_metadata(self):
metadata = {'meta': 'data'}
self.register_uris([
self.get_nova_discovery_mock_dict(),
dict(method='GET',
@ -67,10 +68,11 @@ class TestServerSetMetadata(base.TestCase):
uri=self.get_mock_url(
'compute', 'public',
append=['servers', self.fake_server['id'], 'metadata']),
validate=dict(json={'metadata': {'meta': 'data'}}),
status_code=200),
validate=dict(json={'metadata': metadata}),
status_code=200,
json={'metadata': metadata}),
])
self.cloud.set_server_metadata(self.server_id, {'meta': 'data'})
self.cloud.set_server_metadata(self.server_id, metadata)
self.assert_calls()

View File

@ -9,6 +9,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from openstack.compute.v2 import _proxy
from openstack.compute.v2 import availability_zone as az
@ -448,6 +449,33 @@ class TestComputeProxy(test_proxy_base.TestProxyBase):
method_args=["value", "key"],
expected_args=[self.proxy, "key"])
def test_create_image(self):
metadata = {'k1': 'v1'}
with mock.patch('openstack.compute.v2.server.Server.create_image') \
as ci_mock:
ci_mock.return_value = 'image_id'
connection_mock = mock.Mock()
connection_mock.get_image = mock.Mock(return_value='image')
connection_mock.wait_for_image = mock.Mock()
self.proxy._connection = connection_mock
rsp = self.proxy.create_server_image(
'server', 'image_name', metadata, wait=True, timeout=1)
ci_mock.assert_called_with(
self.proxy,
'image_name',
metadata
)
self.proxy._connection.get_image.assert_called_with('image_id')
self.proxy._connection.wait_for_image.assert_called_with(
'image',
timeout=1)
self.assertEqual(connection_mock.wait_for_image(), rsp)
def test_server_group_create(self):
self.verify_create(self.proxy.create_server_group,
server_group.ServerGroup)

View File

@ -124,6 +124,7 @@ class TestServer(base.TestCase):
self.resp = mock.Mock()
self.resp.body = None
self.resp.json = mock.Mock(return_value=self.resp.body)
self.resp.status_code = 200
self.sess = mock.Mock()
self.sess.post = mock.Mock(return_value=self.resp)
@ -395,30 +396,88 @@ class TestServer(base.TestCase):
self.sess.post.assert_called_with(
url, json=body, headers=headers, microversion=None)
def test_create_image(self):
def test_create_image_header(self):
sot = server.Server(**EXAMPLE)
name = 'noo'
metadata = {'nu': 'image', 'created': 'today'}
self.assertIsNone(sot.create_image(self.sess, name, metadata))
url = 'servers/IDENTIFIER/action'
body = {"createImage": {'name': name, 'metadata': metadata}}
headers = {'Accept': ''}
rsp = mock.Mock()
rsp.json.return_value = None
rsp.headers = {'Location': 'dummy/dummy2'}
rsp.status_code = 200
self.sess.post.return_value = rsp
self.endpoint_data = mock.Mock(spec=['min_microversion',
'max_microversion'],
min_microversion=None,
max_microversion='2.44')
self.sess.get_endpoint_data.return_value = self.endpoint_data
image_id = sot.create_image(self.sess, name, metadata)
self.sess.post.assert_called_with(
url, json=body, headers=headers, microversion=None)
self.assertEqual('dummy2', image_id)
def test_create_image_microver(self):
sot = server.Server(**EXAMPLE)
name = 'noo'
metadata = {'nu': 'image', 'created': 'today'}
url = 'servers/IDENTIFIER/action'
body = {"createImage": {'name': name, 'metadata': metadata}}
headers = {'Accept': ''}
rsp = mock.Mock()
rsp.json.return_value = {'image_id': 'dummy3'}
rsp.headers = {'Location': 'dummy/dummy2'}
rsp.status_code = 200
self.sess.post.return_value = rsp
self.endpoint_data = mock.Mock(spec=['min_microversion',
'max_microversion'],
min_microversion='2.1',
max_microversion='2.56')
self.sess.get_endpoint_data.return_value = self.endpoint_data
image_id = sot.create_image(self.sess, name, metadata)
self.sess.post.assert_called_with(
url, json=body, headers=headers, microversion=None)
url, json=body, headers=headers, microversion='2.45')
self.assertEqual('dummy3', image_id)
def test_create_image_minimal(self):
sot = server.Server(**EXAMPLE)
name = 'noo'
self.assertIsNone(self.resp.body, sot.create_image(self.sess, name))
url = 'servers/IDENTIFIER/action'
body = {"createImage": {'name': name}}
headers = {'Accept': ''}
rsp = mock.Mock()
rsp.json.return_value = None
rsp.headers = {'Location': 'dummy/dummy2'}
rsp.status_code = 200
self.sess.post.return_value = rsp
self.endpoint_data = mock.Mock(spec=['min_microversion',
'max_microversion'],
min_microversion='2.1',
max_microversion='2.56')
self.sess.get_endpoint_data.return_value = self.endpoint_data
self.assertIsNone(self.resp.body, sot.create_image(self.sess, name))
self.sess.post.assert_called_with(
url, json=body, headers=headers, microversion=None)
url, json=body, headers=headers, microversion='2.45')
def test_add_security_group(self):
sot = server.Server(**EXAMPLE)