Split OpenStackCloud into reasonable pieces

With over 12k lines of code in openstackcloud class it is hardly 
manageable. Split methods on a service base into corresponding Mixings.

It still make sense to think how individual mixins are structured (it is 
not very nice, when function in an object use property of self, which is 
not defined in the class, but exists only in a resulting mixed object).
Initialization of individual Mixins in the connection should be also 
reviewed

Change-Id: I81050d66e3a2dc0997e3e7620ff2c4e1891bfeb7
This commit is contained in:
Artem Goncharov 2019-03-26 16:26:48 +01:00
parent ae059c5648
commit 1a4ed826b9
21 changed files with 12507 additions and 11766 deletions

View File

@ -0,0 +1,718 @@
# 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.
# import types so that we can reference ListType in sphinx param declarations.
# We can't just use list, because sphinx gets confused by
# openstack.resource.Resource.list and openstack.resource2.Resource.list
import jsonpatch
import types # noqa
import warnings
from openstack.cloud import exc
from openstack.cloud import _normalize
from openstack.cloud import _utils
from openstack import utils
class BaremetalCloudMixin(_normalize.Normalizer):
@property
def _baremetal_client(self):
if 'baremetal' not in self._raw_clients:
client = self._get_raw_client('baremetal')
# Do this to force version discovery. We need to do that, because
# the endpoint-override trick we do for neutron because
# ironicclient just appends a /v1 won't work and will break
# keystoneauth - because ironic's versioned discovery endpoint
# is non-compliant and doesn't return an actual version dict.
client = self._get_versioned_client(
'baremetal', min_version=1, max_version='1.latest')
self._raw_clients['baremetal'] = client
return self._raw_clients['baremetal']
def list_nics(self):
"""Return a list of all bare metal ports."""
return [nic._to_munch() for nic in self.baremetal.ports(details=True)]
def list_nics_for_machine(self, uuid):
"""Returns a list of ports present on the machine node.
:param uuid: String representing machine UUID value in
order to identify the machine.
:returns: A list of ports.
"""
# TODO(dtantsur): support node names here.
return [nic._to_munch()
for nic in self.baremetal.ports(details=True, node_id=uuid)]
def get_nic_by_mac(self, mac):
"""Get bare metal NIC by its hardware address (usually MAC)."""
results = [nic._to_munch()
for nic in self.baremetal.ports(address=mac, details=True)]
try:
return results[0]
except IndexError:
return None
def list_machines(self):
"""List Machines.
:returns: list of ``munch.Munch`` representing machines.
"""
return [self._normalize_machine(node._to_munch())
for node in self.baremetal.nodes()]
def get_machine(self, name_or_id):
"""Get Machine by name or uuid
Search the baremetal host out by utilizing the supplied id value
which can consist of a name or UUID.
:param name_or_id: A node name or UUID that will be looked up.
:returns: ``munch.Munch`` representing the node found or None if no
nodes are found.
"""
try:
return self._normalize_machine(
self.baremetal.get_node(name_or_id)._to_munch())
except exc.OpenStackCloudResourceNotFound:
return None
def get_machine_by_mac(self, mac):
"""Get machine by port MAC address
:param mac: Port MAC address to query in order to return a node.
:returns: ``munch.Munch`` representing the node found or None
if the node is not found.
"""
nic = self.get_nic_by_mac(mac)
if nic is None:
return None
else:
return self.get_machine(nic['node_uuid'])
def inspect_machine(self, name_or_id, wait=False, timeout=3600):
"""Inspect a Barmetal machine
Engages the Ironic node inspection behavior in order to collect
metadata about the baremetal machine.
:param name_or_id: String representing machine name or UUID value in
order to identify the machine.
:param wait: Boolean value controlling if the method is to wait for
the desired state to be reached or a failure to occur.
:param timeout: Integer value, defautling to 3600 seconds, for the$
wait state to reach completion.
:returns: ``munch.Munch`` representing the current state of the machine
upon exit of the method.
"""
return_to_available = False
node = self.baremetal.get_node(name_or_id)
# NOTE(TheJulia): If in available state, we can do this. However,
# we need to to move the machine back to manageable first.
if node.provision_state == 'available':
if node.instance_id:
raise exc.OpenStackCloudException(
"Refusing to inspect available machine %(node)s "
"which is associated with an instance "
"(instance_uuid %(inst)s)" %
{'node': node.id, 'inst': node.instance_id})
return_to_available = True
# NOTE(TheJulia): Changing available machine to managedable state
# and due to state transitions we need to until that transition has
# completed.
node = self.baremetal.set_node_provision_state(node, 'manage',
wait=True,
timeout=timeout)
if node.provision_state not in ('manageable', 'inspect failed'):
raise exc.OpenStackCloudException(
"Machine %(node)s must be in 'manageable', 'inspect failed' "
"or 'available' provision state to start inspection, the "
"current state is %(state)s" %
{'node': node.id, 'state': node.provision_state})
node = self.baremetal.set_node_provision_state(node, 'inspect',
wait=True,
timeout=timeout)
if return_to_available:
node = self.baremetal.set_node_provision_state(node, 'provide',
wait=True,
timeout=timeout)
return node._to_munch()
def register_machine(self, nics, wait=False, timeout=3600,
lock_timeout=600, **kwargs):
"""Register Baremetal with Ironic
Allows for the registration of Baremetal nodes with Ironic
and population of pertinant node information or configuration
to be passed to the Ironic API for the node.
This method also creates ports for a list of MAC addresses passed
in to be utilized for boot and potentially network configuration.
If a failure is detected creating the network ports, any ports
created are deleted, and the node is removed from Ironic.
:param nics:
An array of MAC addresses that represent the
network interfaces for the node to be created.
Example::
[
{'mac': 'aa:bb:cc:dd:ee:01'},
{'mac': 'aa:bb:cc:dd:ee:02'}
]
:param wait: Boolean value, defaulting to false, to wait for the
node to reach the available state where the node can be
provisioned. It must be noted, when set to false, the
method will still wait for locks to clear before sending
the next required command.
:param timeout: Integer value, defautling to 3600 seconds, for the
wait state to reach completion.
:param lock_timeout: Integer value, defaulting to 600 seconds, for
locks to clear.
:param kwargs: Key value pairs to be passed to the Ironic API,
including uuid, name, chassis_uuid, driver_info,
parameters.
:raises: OpenStackCloudException on operation error.
:returns: Returns a ``munch.Munch`` representing the new
baremetal node.
"""
msg = ("Baremetal machine node failed to be created.")
port_msg = ("Baremetal machine port failed to be created.")
url = '/nodes'
# TODO(TheJulia): At some point we need to figure out how to
# handle data across when the requestor is defining newer items
# with the older api.
machine = self._baremetal_client.post(url,
json=kwargs,
error_message=msg,
microversion="1.6")
created_nics = []
try:
for row in nics:
payload = {'address': row['mac'],
'node_uuid': machine['uuid']}
nic = self._baremetal_client.post('/ports',
json=payload,
error_message=port_msg)
created_nics.append(nic['uuid'])
except Exception as e:
self.log.debug("ironic NIC registration failed", exc_info=True)
# TODO(mordred) Handle failures here
try:
for uuid in created_nics:
try:
port_url = '/ports/{uuid}'.format(uuid=uuid)
# NOTE(TheJulia): Added in hope that it is logged.
port_msg = ('Failed to delete port {port} for node '
'{node}').format(port=uuid,
node=machine['uuid'])
self._baremetal_client.delete(port_url,
error_message=port_msg)
except Exception:
pass
finally:
version = "1.6"
msg = "Baremetal machine failed to be deleted."
url = '/nodes/{node_id}'.format(
node_id=machine['uuid'])
self._baremetal_client.delete(url,
error_message=msg,
microversion=version)
raise exc.OpenStackCloudException(
"Error registering NICs with the baremetal service: %s"
% str(e))
with _utils.shade_exceptions(
"Error transitioning node to available state"):
if wait:
for count in utils.iterate_timeout(
timeout,
"Timeout waiting for node transition to "
"available state"):
machine = self.get_machine(machine['uuid'])
# Note(TheJulia): Per the Ironic state code, a node
# that fails returns to enroll state, which means a failed
# node cannot be determined at this point in time.
if machine['provision_state'] in ['enroll']:
self.node_set_provision_state(
machine['uuid'], 'manage')
elif machine['provision_state'] in ['manageable']:
self.node_set_provision_state(
machine['uuid'], 'provide')
elif machine['last_error'] is not None:
raise exc.OpenStackCloudException(
"Machine encountered a failure: %s"
% machine['last_error'])
# Note(TheJulia): Earlier versions of Ironic default to
# None and later versions default to available up until
# the introduction of enroll state.
# Note(TheJulia): The node will transition through
# cleaning if it is enabled, and we will wait for
# completion.
elif machine['provision_state'] in ['available', None]:
break
else:
if machine['provision_state'] in ['enroll']:
self.node_set_provision_state(machine['uuid'], 'manage')
# Note(TheJulia): We need to wait for the lock to clear
# before we attempt to set the machine into provide state
# which allows for the transition to available.
for count in utils.iterate_timeout(
lock_timeout,
"Timeout waiting for reservation to clear "
"before setting provide state"):
machine = self.get_machine(machine['uuid'])
if (machine['reservation'] is None
and machine['provision_state'] != 'enroll'):
# NOTE(TheJulia): In this case, the node has
# has moved on from the previous state and is
# likely not being verified, as no lock is
# present on the node.
self.node_set_provision_state(
machine['uuid'], 'provide')
machine = self.get_machine(machine['uuid'])
break
elif machine['provision_state'] in [
'cleaning',
'available']:
break
elif machine['last_error'] is not None:
raise exc.OpenStackCloudException(
"Machine encountered a failure: %s"
% machine['last_error'])
if not isinstance(machine, str):
return self._normalize_machine(machine)
else:
return machine
def unregister_machine(self, nics, uuid, wait=False, timeout=600):
"""Unregister Baremetal from Ironic
Removes entries for Network Interfaces and baremetal nodes
from an Ironic API
:param nics: An array of strings that consist of MAC addresses
to be removed.
:param string uuid: The UUID of the node to be deleted.
:param wait: Boolean value, defaults to false, if to block the method
upon the final step of unregistering the machine.
:param timeout: Integer value, representing seconds with a default
value of 600, which controls the maximum amount of
time to block the method's completion on.
:raises: OpenStackCloudException on operation failure.
"""
machine = self.get_machine(uuid)
invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed']
if machine['provision_state'] in invalid_states:
raise exc.OpenStackCloudException(
"Error unregistering node '%s' due to current provision "
"state '%s'" % (uuid, machine['provision_state']))
# NOTE(TheJulia) There is a high possibility of a lock being present
# if the machine was just moved through the state machine. This was
# previously concealed by exception retry logic that detected the
# failure, and resubitted the request in python-ironicclient.
try:
self.wait_for_baremetal_node_lock(machine, timeout=timeout)
except exc.OpenStackCloudException as e:
raise exc.OpenStackCloudException(
"Error unregistering node '%s': Exception occured while"
" waiting to be able to proceed: %s" % (machine['uuid'], e))
for nic in nics:
port_msg = ("Error removing NIC {nic} from baremetal API for "
"node {uuid}").format(nic=nic, uuid=uuid)
port_url = '/ports/detail?address={mac}'.format(mac=nic['mac'])
port = self._baremetal_client.get(port_url, microversion=1.6,
error_message=port_msg)
port_url = '/ports/{uuid}'.format(uuid=port['ports'][0]['uuid'])
_utils._call_client_and_retry(self._baremetal_client.delete,
port_url, retry_on=[409, 503],
error_message=port_msg)
with _utils.shade_exceptions(
"Error unregistering machine {node_id} from the baremetal "
"API".format(node_id=uuid)):
# NOTE(TheJulia): While this should not matter microversion wise,
# ironic assumes all calls without an explicit microversion to be
# version 1.0. Ironic expects to deprecate support for older
# microversions in future releases, as such, we explicitly set
# the version to what we have been using with the client library..
version = "1.6"
msg = "Baremetal machine failed to be deleted"
url = '/nodes/{node_id}'.format(
node_id=uuid)
_utils._call_client_and_retry(self._baremetal_client.delete,
url, retry_on=[409, 503],
error_message=msg,
microversion=version)
if wait:
for count in utils.iterate_timeout(
timeout,
"Timeout waiting for machine to be deleted"):
if not self.get_machine(uuid):
break
def patch_machine(self, name_or_id, patch):
"""Patch Machine Information
This method allows for an interface to manipulate node entries
within Ironic.
:param string name_or_id: A machine name or UUID to be updated.
:param patch:
The JSON Patch document is a list of dictonary objects
that comply with RFC 6902 which can be found at
https://tools.ietf.org/html/rfc6902.
Example patch construction::
patch=[]
patch.append({
'op': 'remove',
'path': '/instance_info'
})
patch.append({
'op': 'replace',
'path': '/name',
'value': 'newname'
})
patch.append({
'op': 'add',
'path': '/driver_info/username',
'value': 'administrator'
})
:raises: OpenStackCloudException on operation error.
:returns: ``munch.Munch`` representing the newly updated node.
"""
node = self.baremetal.get_node(name_or_id)
microversion = node._get_microversion_for(self._baremetal_client,
'commit')
msg = ("Error updating machine via patch operation on node "
"{node}".format(node=name_or_id))
url = '/nodes/{node_id}'.format(node_id=node.id)
return self._normalize_machine(
self._baremetal_client.patch(url,
json=patch,
microversion=microversion,
error_message=msg))
def update_machine(self, name_or_id, **attrs):
"""Update a machine with new configuration information
A user-friendly method to perform updates of a machine, in whole or
part.
:param string name_or_id: A machine name or UUID to be updated.
:param attrs: Attributes to updated on the machine.
:raises: OpenStackCloudException on operation error.
:returns: ``munch.Munch`` containing a machine sub-dictonary consisting
of the updated data returned from the API update operation,
and a list named changes which contains all of the API paths
that received updates.
"""
machine = self.get_machine(name_or_id)
if not machine:
raise exc.OpenStackCloudException(
"Machine update failed to find Machine: %s. " % name_or_id)
new_config = dict(machine, **attrs)
try:
patch = jsonpatch.JsonPatch.from_diff(machine, new_config)
except Exception as e:
raise exc.OpenStackCloudException(
"Machine update failed - Error generating JSON patch object "
"for submission to the API. Machine: %s Error: %s"
% (name_or_id, e))
if not patch:
return dict(
node=machine,
changes=None
)
change_list = [change['path'] for change in patch]
node = self.baremetal.update_node(machine, **attrs)
return dict(
node=self._normalize_machine(node._to_munch()),
changes=change_list
)
def attach_port_to_machine(self, name_or_id, port_name_or_id):
"""Attach a virtual port to the bare metal machine.
:param string name_or_id: A machine name or UUID.
:param string port_name_or_id: A port name or UUID.
Note that this is a Network service port, not a bare metal NIC.
:return: Nothing.
"""
machine = self.get_machine(name_or_id)
port = self.get_port(port_name_or_id)
self.baremetal.attach_vif_to_node(machine, port['id'])
def detach_port_from_machine(self, name_or_id, port_name_or_id):
"""Detach a virtual port from the bare metal machine.
:param string name_or_id: A machine name or UUID.
:param string port_name_or_id: A port name or UUID.
Note that this is a Network service port, not a bare metal NIC.
:return: Nothing.
"""
machine = self.get_machine(name_or_id)
port = self.get_port(port_name_or_id)
self.baremetal.detach_vif_from_node(machine, port['id'])
def list_ports_attached_to_machine(self, name_or_id):
"""List virtual ports attached to the bare metal machine.
:param string name_or_id: A machine name or UUID.
:returns: List of ``munch.Munch`` representing the ports.
"""
machine = self.get_machine(name_or_id)
vif_ids = self.baremetal.list_node_vifs(machine)
return [self.get_port(vif) for vif in vif_ids]
def validate_machine(self, name_or_id, for_deploy=True):
"""Validate parameters of the machine.
:param string name_or_id: The Name or UUID value representing the
baremetal node.
:param bool for_deploy: If ``True``, validate readiness for deployment,
otherwise validate only the power management
properties.
:raises: :exc:`~openstack.exceptions.ValidationException`
"""
if for_deploy:
ifaces = ('boot', 'deploy', 'management', 'power')
else:
ifaces = ('power',)
self.baremetal.validate_node(name_or_id, required=ifaces)
def validate_node(self, uuid):
warnings.warn('validate_node is deprecated, please use '
'validate_machine instead', DeprecationWarning)
self.baremetal.validate_node(uuid)
def node_set_provision_state(self,
name_or_id,
state,
configdrive=None,
wait=False,
timeout=3600):
"""Set Node Provision State
Enables a user to provision a Machine and optionally define a
config drive to be utilized.
:param string name_or_id: The Name or UUID value representing the
baremetal node.
:param string state: The desired provision state for the
baremetal node.
:param string configdrive: An optional URL or file or path
representing the configdrive. In the
case of a directory, the client API
will create a properly formatted
configuration drive file and post the
file contents to the API for
deployment.
:param boolean wait: A boolean value, defaulted to false, to control
if the method will wait for the desire end state
to be reached before returning.
:param integer timeout: Integer value, defaulting to 3600 seconds,
representing the amount of time to wait for
the desire end state to be reached.
:raises: OpenStackCloudException on operation error.
:returns: ``munch.Munch`` representing the current state of the machine
upon exit of the method.
"""
node = self.baremetal.set_node_provision_state(
name_or_id, target=state, config_drive=configdrive,
wait=wait, timeout=timeout)
return node._to_munch()
def set_machine_maintenance_state(
self,
name_or_id,
state=True,
reason=None):
"""Set Baremetal Machine Maintenance State
Sets Baremetal maintenance state and maintenance reason.
:param string name_or_id: The Name or UUID value representing the
baremetal node.
:param boolean state: The desired state of the node. True being in
maintenance where as False means the machine
is not in maintenance mode. This value
defaults to True if not explicitly set.
:param string reason: An optional freeform string that is supplied to
the baremetal API to allow for notation as to why
the node is in maintenance state.
:raises: OpenStackCloudException on operation error.
:returns: None
"""
if state:
self.baremetal.set_node_maintenance(name_or_id, reason)
else:
self.baremetal.unset_node_maintenance(name_or_id)
def remove_machine_from_maintenance(self, name_or_id):
"""Remove Baremetal Machine from Maintenance State
Similarly to set_machine_maintenance_state, this method
removes a machine from maintenance state. It must be noted
that this method simpily calls set_machine_maintenace_state
for the name_or_id requested and sets the state to False.
:param string name_or_id: The Name or UUID value representing the
baremetal node.
:raises: OpenStackCloudException on operation error.
:returns: None
"""
self.baremetal.unset_node_maintenance(name_or_id)
def set_machine_power_on(self, name_or_id):
"""Activate baremetal machine power
This is a method that sets the node power state to "on".
:params string name_or_id: A string representing the baremetal
node to have power turned to an "on"
state.
:raises: OpenStackCloudException on operation error.
:returns: None
"""
self.baremetal.set_node_power_state(name_or_id, 'power on')
def set_machine_power_off(self, name_or_id):
"""De-activate baremetal machine power
This is a method that sets the node power state to "off".
:params string name_or_id: A string representing the baremetal
node to have power turned to an "off"
state.
:raises: OpenStackCloudException on operation error.
:returns:
"""
self.baremetal.set_node_power_state(name_or_id, 'power off')
def set_machine_power_reboot(self, name_or_id):
"""De-activate baremetal machine power
This is a method that sets the node power state to "reboot", which
in essence changes the machine power state to "off", and that back
to "on".
:params string name_or_id: A string representing the baremetal
node to have power turned to an "off"
state.
:raises: OpenStackCloudException on operation error.
:returns: None
"""
self.baremetal.set_node_power_state(name_or_id, 'rebooting')
def activate_node(self, uuid, configdrive=None,
wait=False, timeout=1200):
self.node_set_provision_state(
uuid, 'active', configdrive, wait=wait, timeout=timeout)
def deactivate_node(self, uuid, wait=False,
timeout=1200):
self.node_set_provision_state(
uuid, 'deleted', wait=wait, timeout=timeout)
def set_node_instance_info(self, uuid, patch):
msg = ("Error updating machine via patch operation on node "
"{node}".format(node=uuid))
url = '/nodes/{node_id}'.format(node_id=uuid)
return self._baremetal_client.patch(url,
json=patch,
error_message=msg)
def purge_node_instance_info(self, uuid):
patch = []
patch.append({'op': 'remove', 'path': '/instance_info'})
msg = ("Error updating machine via patch operation on node "
"{node}".format(node=uuid))
url = '/nodes/{node_id}'.format(node_id=uuid)
return self._baremetal_client.patch(url,
json=patch,
error_message=msg)
def wait_for_baremetal_node_lock(self, node, timeout=30):
"""Wait for a baremetal node to have no lock.
DEPRECATED, use ``wait_for_node_reservation`` on the `baremetal` proxy.
:raises: OpenStackCloudException upon client failure.
:returns: None
"""
warnings.warn("The wait_for_baremetal_node_lock call is deprecated "
"in favor of wait_for_node_reservation on the baremetal "
"proxy", DeprecationWarning)
self.baremetal.wait_for_node_reservation(node, timeout)

View File

@ -0,0 +1,870 @@
# 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.
# import types so that we can reference ListType in sphinx param declarations.
# We can't just use list, because sphinx gets confused by
# openstack.resource.Resource.list and openstack.resource2.Resource.list
import types # noqa
import warnings
from openstack.cloud import exc
from openstack.cloud import _normalize
from openstack.cloud import _utils
from openstack import proxy
from openstack import utils
def _no_pending_volumes(volumes):
"""If there are any volumes not in a steady state, don't cache"""
for volume in volumes:
if volume['status'] not in ('available', 'error', 'in-use'):
return False
return True
class BlockStorageCloudMixin(_normalize.Normalizer):
@property
def _volume_client(self):
if 'block-storage' not in self._raw_clients:
client = self._get_raw_client('block-storage')
self._raw_clients['block-storage'] = client
return self._raw_clients['block-storage']
@_utils.cache_on_arguments(should_cache_fn=_no_pending_volumes)
def list_volumes(self, cache=True):
"""List all available volumes.
:returns: A list of volume ``munch.Munch``.
"""
def _list(data):
volumes.extend(data.get('volumes', []))
endpoint = None
for l in data.get('volumes_links', []):
if 'rel' in l and 'next' == l['rel']:
endpoint = l['href']
break
if endpoint:
try:
_list(self._volume_client.get(endpoint))
except exc.OpenStackCloudURINotFound:
# Catch and re-raise here because we are making recursive
# calls and we just have context for the log here
self.log.debug(
"While listing volumes, could not find next link"
" {link}.".format(link=data))
raise
if not cache:
warnings.warn('cache argument to list_volumes is deprecated. Use '
'invalidate instead.')
# Fetching paginated volumes can fails for several reasons, if
# something goes wrong we'll have to start fetching volumes from
# scratch
attempts = 5
for _ in range(attempts):
volumes = []
data = self._volume_client.get('/volumes/detail')
if 'volumes_links' not in data:
# no pagination needed
volumes.extend(data.get('volumes', []))
break
try:
_list(data)
break
except exc.OpenStackCloudURINotFound:
pass
else:
self.log.debug(
"List volumes failed to retrieve all volumes after"
" {attempts} attempts. Returning what we found.".format(
attempts=attempts))
# list volumes didn't complete succesfully so just return what
# we found
return self._normalize_volumes(
self._get_and_munchify(key=None, data=volumes))
@_utils.cache_on_arguments()
def list_volume_types(self, get_extra=True):
"""List all available volume types.
:returns: A list of volume ``munch.Munch``.
"""
data = self._volume_client.get(
'/types',
params=dict(is_public='None'),
error_message='Error fetching volume_type list')
return self._normalize_volume_types(
self._get_and_munchify('volume_types', data))
def get_volume(self, name_or_id, filters=None):
"""Get a volume by name or ID.
:param name_or_id: Name or ID of the volume.
:param filters:
A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example::
{
'last_name': 'Smith',
'other': {
'gender': 'Female'
}
}
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A volume ``munch.Munch`` or None if no matching volume is
found.
"""
return _utils._get_entity(self, 'volume', name_or_id, filters)
def get_volume_by_id(self, id):
""" Get a volume by ID
:param id: ID of the volume.
:returns: A volume ``munch.Munch``.
"""
data = self._volume_client.get(
'/volumes/{id}'.format(id=id),
error_message="Error getting volume with ID {id}".format(id=id)
)
volume = self._normalize_volume(
self._get_and_munchify('volume', data))
return volume
def get_volume_type(self, name_or_id, filters=None):
"""Get a volume type by name or ID.
:param name_or_id: Name or ID of the volume.
:param filters:
A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example::
{
'last_name': 'Smith',
'other': {
'gender': 'Female'
}
}
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A volume ``munch.Munch`` or None if no matching volume is
found.
"""
return _utils._get_entity(
self, 'volume_type', name_or_id, filters)
def create_volume(
self, size,
wait=True, timeout=None, image=None, bootable=None, **kwargs):
"""Create a volume.
:param size: Size, in GB of the volume to create.
:param name: (optional) Name for the volume.
:param description: (optional) Name for the volume.
:param wait: If true, waits for volume to be created.
:param timeout: Seconds to wait for volume creation. None is forever.
:param image: (optional) Image name, ID or object from which to create
the volume
:param bootable: (optional) Make this volume bootable. If set, wait
will also be set to true.
:param kwargs: Keyword arguments as expected for cinder client.
:returns: The created volume object.
:raises: OpenStackCloudTimeout if wait time exceeded.
:raises: OpenStackCloudException on operation error.
"""
if bootable is not None:
wait = True
if image:
image_obj = self.get_image(image)
if not image_obj:
raise exc.OpenStackCloudException(
"Image {image} was requested as the basis for a new"
" volume, but was not found on the cloud".format(
image=image))
kwargs['imageRef'] = image_obj['id']
kwargs = self._get_volume_kwargs(kwargs)
kwargs['size'] = size
payload = dict(volume=kwargs)
if 'scheduler_hints' in kwargs:
payload['OS-SCH-HNT:scheduler_hints'] = kwargs.pop(
'scheduler_hints', None)
data = self._volume_client.post(
'/volumes',
json=dict(payload),
error_message='Error in creating volume')
volume = self._get_and_munchify('volume', data)
self.list_volumes.invalidate(self)
if volume['status'] == 'error':
raise exc.OpenStackCloudException("Error in creating volume")
if wait:
vol_id = volume['id']
for count in utils.iterate_timeout(
timeout,
"Timeout waiting for the volume to be available."):
volume = self.get_volume(vol_id)
if not volume:
continue
if volume['status'] == 'available':
if bootable is not None:
self.set_volume_bootable(volume, bootable=bootable)
# no need to re-fetch to update the flag, just set it.
volume['bootable'] = bootable
return volume
if volume['status'] == 'error':
raise exc.OpenStackCloudException("Error creating volume")
return self._normalize_volume(volume)
def update_volume(self, name_or_id, **kwargs):
kwargs = self._get_volume_kwargs(kwargs)
volume = self.get_volume(name_or_id)
if not volume:
raise exc.OpenStackCloudException(
"Volume %s not found." % name_or_id)
data = self._volume_client.put(
'/volumes/{volume_id}'.format(volume_id=volume.id),
json=dict({'volume': kwargs}),
error_message='Error updating volume')
self.list_volumes.invalidate(self)
return self._normalize_volume(self._get_and_munchify('volume', data))
def set_volume_bootable(self, name_or_id, bootable=True):
"""Set a volume's bootable flag.
:param name_or_id: Name, unique ID of the volume or a volume dict.
:param bool bootable: Whether the volume should be bootable.
(Defaults to True)
:raises: OpenStackCloudTimeout if wait time exceeded.
:raises: OpenStackCloudException on operation error.
"""
volume = self.get_volume(name_or_id)
if not volume:
raise exc.OpenStackCloudException(
"Volume {name_or_id} does not exist".format(
name_or_id=name_or_id))
self._volume_client.post(
'volumes/{id}/action'.format(id=volume['id']),
json={'os-set_bootable': {'bootable': bootable}},
error_message="Error setting bootable on volume {volume}".format(
volume=volume['id'])
)
def delete_volume(self, name_or_id=None, wait=True, timeout=None,
force=False):
"""Delete a volume.
:param name_or_id: Name or unique ID of the volume.
:param wait: If true, waits for volume to be deleted.
:param timeout: Seconds to wait for volume deletion. None is forever.
:param force: Force delete volume even if the volume is in deleting
or error_deleting state.
:raises: OpenStackCloudTimeout if wait time exceeded.
:raises: OpenStackCloudException on operation error.
"""
self.list_volumes.invalidate(self)
volume = self.get_volume(name_or_id)
if not volume:
self.log.debug(
"Volume %(name_or_id)s does not exist",
{'name_or_id': name_or_id},
exc_info=True)
return False
with _utils.shade_exceptions("Error in deleting volume"):
try:
if force:
self._volume_client.post(
'volumes/{id}/action'.format(id=volume['id']),
json={'os-force_delete': None})
else:
self._volume_client.delete(
'volumes/{id}'.format(id=volume['id']))
except exc.OpenStackCloudURINotFound:
self.log.debug(
"Volume {id} not found when deleting. Ignoring.".format(
id=volume['id']))
return False
self.list_volumes.invalidate(self)
if wait:
for count in utils.iterate_timeout(
timeout,
"Timeout waiting for the volume to be deleted."):
if not self.get_volume(volume['id']):
break
return True
def get_volumes(self, server, cache=True):
volumes = []
for volume in self.list_volumes(cache=cache):
for attach in volume['attachments']:
if attach['server_id'] == server['id']:
volumes.append(volume)
return volumes
def get_volume_limits(self, name_or_id=None):
""" Get volume limits for a project
:param name_or_id: (optional) project name or ID to get limits for
if different from the current project
:raises: OpenStackCloudException if it's not a valid project
:returns: Munch object with the limits
"""
params = {}
project_id = None
error_msg = "Failed to get limits"
if name_or_id:
proj = self.get_project(name_or_id)
if not proj:
raise exc.OpenStackCloudException("project does not exist")
project_id = proj.id
params['tenant_id'] = project_id
error_msg = "{msg} for the project: {project} ".format(
msg=error_msg, project=name_or_id)
data = self._volume_client.get('/limits', params=params)
limits = self._get_and_munchify('limits', data)
return limits
def get_volume_id(self, name_or_id):
volume = self.get_volume(name_or_id)
if volume:
return volume['id']
return None
def volume_exists(self, name_or_id):
return self.get_volume(name_or_id) is not None
def get_volume_attach_device(self, volume, server_id):
"""Return the device name a volume is attached to for a server.
This can also be used to verify if a volume is attached to
a particular server.
:param volume: Volume dict
:param server_id: ID of server to check
:returns: Device name if attached, None if volume is not attached.
"""
for attach in volume['attachments']:
if server_id == attach['server_id']:
return attach['device']
return None
def detach_volume(self, server, volume, wait=True, timeout=None):
"""Detach a volume from a server.
:param server: The server dict to detach from.
:param volume: The volume dict to detach.
:param wait: If true, waits for volume to be detached.
:param timeout: Seconds to wait for volume detachment. None is forever.
:raises: OpenStackCloudTimeout if wait time exceeded.
:raises: OpenStackCloudException on operation error.
"""
proxy._json_response(self.compute.delete(
'/servers/{server_id}/os-volume_attachments/{volume_id}'.format(
server_id=server['id'], volume_id=volume['id'])),
error_message=(
"Error detaching volume {volume} from server {server}".format(
volume=volume['id'], server=server['id'])))
if wait:
for count in utils.iterate_timeout(
timeout,
"Timeout waiting for volume %s to detach." % volume['id']):
try:
vol = self.get_volume(volume['id'])
except Exception:
self.log.debug(
"Error getting volume info %s", volume['id'],
exc_info=True)
continue
if vol['status'] == 'available':
return
if vol['status'] == 'error':
raise exc.OpenStackCloudException(
"Error in detaching volume %s" % volume['id']
)
def attach_volume(self, server, volume, device=None,
wait=True, timeout=None):
"""Attach a volume to a server.
This will attach a volume, described by the passed in volume
dict (as returned by get_volume()), to the server described by
the passed in server dict (as returned by get_server()) on the
named device on the server.
If the volume is already attached to the server, or generally not
available, then an exception is raised. To re-attach to a server,
but under a different device, the user must detach it first.
:param server: The server dict to attach to.
:param volume: The volume dict to attach.
:param device: The device name where the volume will attach.
:param wait: If true, waits for volume to be attached.
:param timeout: Seconds to wait for volume attachment. None is forever.
:returns: a volume attachment object.
:raises: OpenStackCloudTimeout if wait time exceeded.
:raises: OpenStackCloudException on operation error.
"""
dev = self.get_volume_attach_device(volume, server['id'])
if dev:
raise exc.OpenStackCloudException(
"Volume %s already attached to server %s on device %s"
% (volume['id'], server['id'], dev)
)
if volume['status'] != 'available':
raise exc.OpenStackCloudException(
"Volume %s is not available. Status is '%s'"
% (volume['id'], volume['status'])
)
payload = {'volumeId': volume['id']}
if device:
payload['device'] = device
data = proxy._json_response(
self.compute.post(
'/servers/{server_id}/os-volume_attachments'.format(
server_id=server['id']),
json=dict(volumeAttachment=payload)),
error_message="Error attaching volume {volume_id} to server "
"{server_id}".format(volume_id=volume['id'],
server_id=server['id']))
if wait:
for count in utils.iterate_timeout(
timeout,
"Timeout waiting for volume %s to attach." % volume['id']):
try:
self.list_volumes.invalidate(self)
vol = self.get_volume(volume['id'])
except Exception:
self.log.debug(
"Error getting volume info %s", volume['id'],
exc_info=True)
continue
if self.get_volume_attach_device(vol, server['id']):
break
# TODO(Shrews) check to see if a volume can be in error status
# and also attached. If so, we should move this
# above the get_volume_attach_device call
if vol['status'] == 'error':
raise exc.OpenStackCloudException(
"Error in attaching volume %s" % volume['id']
)
return self._normalize_volume_attachment(
self._get_and_munchify('volumeAttachment', data))
def _get_volume_kwargs(self, kwargs):
name = kwargs.pop('name', kwargs.pop('display_name', None))
description = kwargs.pop('description',
kwargs.pop('display_description', None))
if name:
if self._is_client_version('volume', 2):
kwargs['name'] = name
else:
kwargs['display_name'] = name
if description:
if self._is_client_version('volume', 2):
kwargs['description'] = description
else:
kwargs['display_description'] = description
return kwargs
@_utils.valid_kwargs('name', 'display_name',
'description', 'display_description')
def create_volume_snapshot(self, volume_id, force=False,
wait=True, timeout=None, **kwargs):
"""Create a volume.
:param volume_id: the ID of the volume to snapshot.
:param force: If set to True the snapshot will be created even if the
volume is attached to an instance, if False it will not
:param name: name of the snapshot, one will be generated if one is
not provided
:param description: description of the snapshot, one will be generated
if one is not provided
:param wait: If true, waits for volume snapshot to be created.
:param timeout: Seconds to wait for volume snapshot creation. None is
forever.
:returns: The created volume object.
:raises: OpenStackCloudTimeout if wait time exceeded.
:raises: OpenStackCloudException on operation error.
"""
kwargs = self._get_volume_kwargs(kwargs)
payload = {'volume_id': volume_id, 'force': force}
payload.update(kwargs)
data = self._volume_client.post(
'/snapshots',
json=dict(snapshot=payload),
error_message="Error creating snapshot of volume "
"{volume_id}".format(volume_id=volume_id))
snapshot = self._get_and_munchify('snapshot', data)
if wait:
snapshot_id = snapshot['id']
for count in utils.iterate_timeout(
timeout,
"Timeout waiting for the volume snapshot to be available."
):
snapshot = self.get_volume_snapshot_by_id(snapshot_id)
if snapshot['status'] == 'available':
break
if snapshot['status'] == 'error':
raise exc.OpenStackCloudException(
"Error in creating volume snapshot")
# TODO(mordred) need to normalize snapshots. We were normalizing them
# as volumes, which is an error. They need to be normalized as
# volume snapshots, which are completely different objects
return snapshot
def get_volume_snapshot_by_id(self, snapshot_id):
"""Takes a snapshot_id and gets a dict of the snapshot
that maches that ID.
Note: This is more efficient than get_volume_snapshot.
param: snapshot_id: ID of the volume snapshot.
"""
data = self._volume_client.get(
'/snapshots/{snapshot_id}'.format(snapshot_id=snapshot_id),
error_message="Error getting snapshot "
"{snapshot_id}".format(snapshot_id=snapshot_id))
return self._normalize_volume(
self._get_and_munchify('snapshot', data))
def get_volume_snapshot(self, name_or_id, filters=None):
"""Get a volume by name or ID.
:param name_or_id: Name or ID of the volume snapshot.
:param filters:
A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example::
{
'last_name': 'Smith',
'other': {
'gender': 'Female'
}
}
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A volume ``munch.Munch`` or None if no matching volume is
found.
"""
return _utils._get_entity(self, 'volume_snapshot', name_or_id,
filters)
def create_volume_backup(self, volume_id, name=None, description=None,
force=False, wait=True, timeout=None):
"""Create a volume backup.
:param volume_id: the ID of the volume to backup.
:param name: name of the backup, one will be generated if one is
not provided
:param description: description of the backup, one will be generated
if one is not provided
:param force: If set to True the backup will be created even if the
volume is attached to an instance, if False it will not
:param wait: If true, waits for volume backup to be created.
:param timeout: Seconds to wait for volume backup creation. None is
forever.
:returns: The created volume backup object.
:raises: OpenStackCloudTimeout if wait time exceeded.
:raises: OpenStackCloudException on operation error.
"""
payload = {
'name': name,
'volume_id': volume_id,
'description': description,
'force': force,
}
data = self._volume_client.post(
'/backups', json=dict(backup=payload),
error_message="Error creating backup of volume "
"{volume_id}".format(volume_id=volume_id))
backup = self._get_and_munchify('backup', data)
if wait:
backup_id = backup['id']
msg = ("Timeout waiting for the volume backup {} to be "
"available".format(backup_id))
for _ in utils.iterate_timeout(timeout, msg):
backup = self.get_volume_backup(backup_id)
if backup['status'] == 'available':
break
if backup['status'] == 'error':
raise exc.OpenStackCloudException(
"Error in creating volume backup {id}".format(
id=backup_id))
return backup
def get_volume_backup(self, name_or_id, filters=None):
"""Get a volume backup by name or ID.
:returns: A backup ``munch.Munch`` or None if no matching backup is
found.
"""
return _utils._get_entity(self, 'volume_backup', name_or_id,
filters)
def list_volume_snapshots(self, detailed=True, search_opts=None):
"""List all volume snapshots.
:returns: A list of volume snapshots ``munch.Munch``.
"""
endpoint = '/snapshots/detail' if detailed else '/snapshots'
data = self._volume_client.get(
endpoint,
params=search_opts,
error_message="Error getting a list of snapshots")
return self._get_and_munchify('snapshots', data)
def list_volume_backups(self, detailed=True, search_opts=None):
"""
List all volume backups.
:param bool detailed: Also list details for each entry
:param dict search_opts: Search options
A dictionary of meta data to use for further filtering. Example::
{
'name': 'my-volume-backup',
'status': 'available',
'volume_id': 'e126044c-7b4c-43be-a32a-c9cbbc9ddb56',
'all_tenants': 1
}
:returns: A list of volume backups ``munch.Munch``.
"""
endpoint = '/backups/detail' if detailed else '/backups'
data = self._volume_client.get(
endpoint, params=search_opts,
error_message="Error getting a list of backups")
return self._get_and_munchify('backups', data)
def delete_volume_backup(self, name_or_id=None, force=False, wait=False,
timeout=None):
"""Delete a volume backup.
:param name_or_id: Name or unique ID of the volume backup.
:param force: Allow delete in state other than error or available.
:param wait: If true, waits for volume backup to be deleted.
:param timeout: Seconds to wait for volume backup deletion. None is
forever.
:raises: OpenStackCloudTimeout if wait time exceeded.
:raises: OpenStackCloudException on operation error.
"""
volume_backup = self.get_volume_backup(name_or_id)
if not volume_backup:
return False
msg = "Error in deleting volume backup"
if force:
self._volume_client.post(
'/backups/{backup_id}/action'.format(
backup_id=volume_backup['id']),
json={'os-force_delete': None},
error_message=msg)
else:
self._volume_client.delete(
'/backups/{backup_id}'.format(
backup_id=volume_backup['id']),
error_message=msg)
if wait:
msg = "Timeout waiting for the volume backup to be deleted."
for count in utils.iterate_timeout(timeout, msg):
if not self.get_volume_backup(volume_backup['id']):
break
return True
def delete_volume_snapshot(self, name_or_id=None, wait=False,
timeout=None):
"""Delete a volume snapshot.
:param name_or_id: Name or unique ID of the volume snapshot.
:param wait: If true, waits for volume snapshot to be deleted.
:param timeout: Seconds to wait for volume snapshot deletion. None is
forever.
:raises: OpenStackCloudTimeout if wait time exceeded.
:raises: OpenStackCloudException on operation error.
"""
volumesnapshot = self.get_volume_snapshot(name_or_id)
if not volumesnapshot:
return False
self._volume_client.delete(
'/snapshots/{snapshot_id}'.format(
snapshot_id=volumesnapshot['id']),
error_message="Error in deleting volume snapshot")
if wait:
for count in utils.iterate_timeout(
timeout,
"Timeout waiting for the volume snapshot to be deleted."):
if not self.get_volume_snapshot(volumesnapshot['id']):
break
return True
def search_volumes(self, name_or_id=None, filters=None):
volumes = self.list_volumes()
return _utils._filter_list(
volumes, name_or_id, filters)
def search_volume_snapshots(self, name_or_id=None, filters=None):
volumesnapshots = self.list_volume_snapshots()
return _utils._filter_list(
volumesnapshots, name_or_id, filters)
def search_volume_backups(self, name_or_id=None, filters=None):
volume_backups = self.list_volume_backups()
return _utils._filter_list(
volume_backups, name_or_id, filters)
def search_volume_types(
self, name_or_id=None, filters=None, get_extra=True):
volume_types = self.list_volume_types(get_extra=get_extra)
return _utils._filter_list(volume_types, name_or_id, filters)
def get_volume_type_access(self, name_or_id):
"""Return a list of volume_type_access.
:param name_or_id: Name or ID of the volume type.
:raises: OpenStackCloudException on operation error.
"""
volume_type = self.get_volume_type(name_or_id)
if not volume_type:
raise exc.OpenStackCloudException(
"VolumeType not found: %s" % name_or_id)
data = self._volume_client.get(
'/types/{id}/os-volume-type-access'.format(id=volume_type.id),
error_message="Unable to get volume type access"
" {name}".format(name=name_or_id))
return self._normalize_volume_type_accesses(
self._get_and_munchify('volume_type_access', data))
def add_volume_type_access(self, name_or_id, project_id):
"""Grant access on a volume_type to a project.
:param name_or_id: ID or name of a volume_type
:param project_id: A project id
NOTE: the call works even if the project does not exist.
:raises: OpenStackCloudException on operation error.
"""
volume_type = self.get_volume_type(name_or_id)
if not volume_type:
raise exc.OpenStackCloudException(
"VolumeType not found: %s" % name_or_id)
with _utils.shade_exceptions():
payload = {'project': project_id}
self._volume_client.post(
'/types/{id}/action'.format(id=volume_type.id),
json=dict(addProjectAccess=payload),
error_message="Unable to authorize {project} "
"to use volume type {name}".format(
name=name_or_id, project=project_id))
def remove_volume_type_access(self, name_or_id, project_id):
"""Revoke access on a volume_type to a project.
:param name_or_id: ID or name of a volume_type
:param project_id: A project id
:raises: OpenStackCloudException on operation error.
"""
volume_type = self.get_volume_type(name_or_id)
if not volume_type:
raise exc.OpenStackCloudException(
"VolumeType not found: %s" % name_or_id)
with _utils.shade_exceptions():
payload = {'project': project_id}
self._volume_client.post(
'/types/{id}/action'.format(id=volume_type.id),
json=dict(removeProjectAccess=payload),
error_message="Unable to revoke {project} "
"to use volume type {name}".format(
name=name_or_id, project=project_id))

View File

@ -0,0 +1,567 @@
# 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.
# import types so that we can reference ListType in sphinx param declarations.
# We can't just use list, because sphinx gets confused by
# openstack.resource.Resource.list and openstack.resource2.Resource.list
import types # noqa
from openstack.cloud import exc
from openstack.cloud import _normalize
from openstack.cloud import _utils
from openstack import utils
class ClusteringCloudMixin(_normalize.Normalizer):
@property
def _clustering_client(self):
if 'clustering' not in self._raw_clients:
clustering_client = self._get_versioned_client(
'clustering', min_version=1, max_version='1.latest')
self._raw_clients['clustering'] = clustering_client
return self._raw_clients['clustering']
def create_cluster(self, name, profile, config=None, desired_capacity=0,
max_size=None, metadata=None, min_size=None,
timeout=None):
profile = self.get_cluster_profile(profile)
profile_id = profile['id']
body = {
'desired_capacity': desired_capacity,
'name': name,
'profile_id': profile_id
}
if config is not None:
body['config'] = config
if max_size is not None:
body['max_size'] = max_size
if metadata is not None:
body['metadata'] = metadata
if min_size is not None:
body['min_size'] = min_size
if timeout is not None:
body['timeout'] = timeout
data = self._clustering_client.post(
'/clusters', json={'cluster': body},
error_message="Error creating cluster {name}".format(name=name))
return self._get_and_munchify(key=None, data=data)
def set_cluster_metadata(self, name_or_id, metadata):
cluster = self.get_cluster(name_or_id)
if not cluster:
raise exc.OpenStackCloudException(
'Invalid Cluster {cluster}'.format(cluster=name_or_id))
self._clustering_client.post(
'/clusters/{cluster_id}/metadata'.format(cluster_id=cluster['id']),
json={'metadata': metadata},
error_message='Error updating cluster metadata')
def get_cluster_by_id(self, cluster_id):
try:
data = self._clustering_client.get(
"/clusters/{cluster_id}".format(cluster_id=cluster_id),
error_message="Error fetching cluster {name}".format(
name=cluster_id))
return self._get_and_munchify('cluster', data)
except Exception:
return None
def get_cluster(self, name_or_id, filters=None):
return _utils._get_entity(
cloud=self, resource='cluster',
name_or_id=name_or_id, filters=filters)
def update_cluster(self, name_or_id, new_name=None,
profile_name_or_id=None, config=None, metadata=None,
timeout=None, profile_only=False):
old_cluster = self.get_cluster(name_or_id)
if old_cluster is None:
raise exc.OpenStackCloudException(
'Invalid Cluster {cluster}'.format(cluster=name_or_id))
cluster = {
'profile_only': profile_only
}
if config is not None:
cluster['config'] = config
if metadata is not None:
cluster['metadata'] = metadata
if profile_name_or_id is not None:
profile = self.get_cluster_profile(profile_name_or_id)
if profile is None:
raise exc.OpenStackCloudException(
'Invalid Cluster Profile {profile}'.format(
profile=profile_name_or_id))
cluster['profile_id'] = profile.id
if timeout is not None:
cluster['timeout'] = timeout
if new_name is not None:
cluster['name'] = new_name
data = self._clustering_client.patch(
"/clusters/{cluster_id}".format(cluster_id=old_cluster['id']),
json={'cluster': cluster},
error_message="Error updating cluster "
"{name}".format(name=name_or_id))
return self._get_and_munchify(key=None, data=data)
def delete_cluster(self, name_or_id):
cluster = self.get_cluster(name_or_id)
if cluster is None:
self.log.debug("Cluster %s not found for deleting", name_or_id)
return False
for policy in self.list_policies_on_cluster(name_or_id):
detach_policy = self.get_cluster_policy_by_id(
policy['policy_id'])
self.detach_policy_from_cluster(cluster, detach_policy)
for receiver in self.list_cluster_receivers():
if cluster["id"] == receiver["cluster_id"]:
self.delete_cluster_receiver(receiver["id"], wait=True)
self._clustering_client.delete(
"/clusters/{cluster_id}".format(cluster_id=name_or_id),
error_message="Error deleting cluster {name}".format(
name=name_or_id))
return True
def search_clusters(self, name_or_id=None, filters=None):
clusters = self.list_clusters()
return _utils._filter_list(clusters, name_or_id, filters)
def list_clusters(self):
try:
data = self._clustering_client.get(
'/clusters',
error_message="Error fetching clusters")
return self._get_and_munchify('clusters', data)
except exc.OpenStackCloudURINotFound as e:
self.log.debug(str(e), exc_info=True)
return []
def attach_policy_to_cluster(self, name_or_id, policy_name_or_id,
is_enabled):
cluster = self.get_cluster(name_or_id)
policy = self.get_cluster_policy(policy_name_or_id)
if cluster is None:
raise exc.OpenStackCloudException(
'Cluster {cluster} not found for attaching'.format(
cluster=name_or_id))
if policy is None:
raise exc.OpenStackCloudException(
'Policy {policy} not found for attaching'.format(
policy=policy_name_or_id))
body = {
'policy_id': policy['id'],
'enabled': is_enabled
}
self._clustering_client.post(
"/clusters/{cluster_id}/actions".format(cluster_id=cluster['id']),
error_message="Error attaching policy {policy} to cluster "
"{cluster}".format(
policy=policy['id'],
cluster=cluster['id']),
json={'policy_attach': body})
return True
def detach_policy_from_cluster(
self, name_or_id, policy_name_or_id, wait=False, timeout=3600):
cluster = self.get_cluster(name_or_id)
policy = self.get_cluster_policy(policy_name_or_id)
if cluster is None:
raise exc.OpenStackCloudException(
'Cluster {cluster} not found for detaching'.format(
cluster=name_or_id))
if policy is None:
raise exc.OpenStackCloudException(
'Policy {policy} not found for detaching'.format(
policy=policy_name_or_id))
body = {'policy_id': policy['id']}
self._clustering_client.post(
"/clusters/{cluster_id}/actions".format(cluster_id=cluster['id']),
error_message="Error detaching policy {policy} from cluster "
"{cluster}".format(
policy=policy['id'],
cluster=cluster['id']),
json={'policy_detach': body})
if not wait:
return True
value = []
for count in utils.iterate_timeout(
timeout, "Timeout waiting for cluster policy to detach"):
# TODO(bjjohnson) This logic will wait until there are no policies.
# Since we're detaching a specific policy, checking to make sure
# that policy is not in the list of policies would be better.
policy_status = self.get_cluster_by_id(cluster['id'])['policies']
if policy_status == value:
break
return True
def update_policy_on_cluster(self, name_or_id, policy_name_or_id,
is_enabled):
cluster = self.get_cluster(name_or_id)
policy = self.get_cluster_policy(policy_name_or_id)
if cluster is None:
raise exc.OpenStackCloudException(
'Cluster {cluster} not found for updating'.format(
cluster=name_or_id))
if policy is None:
raise exc.OpenStackCloudException(
'Policy {policy} not found for updating'.format(
policy=policy_name_or_id))
body = {
'policy_id': policy['id'],
'enabled': is_enabled
}
self._clustering_client.post(
"/clusters/{cluster_id}/actions".format(cluster_id=cluster['id']),
error_message="Error updating policy {policy} on cluster "
"{cluster}".format(
policy=policy['id'],
cluster=cluster['id']),
json={'policy_update': body})
return True
def get_policy_on_cluster(self, name_or_id, policy_name_or_id):
try:
policy = self._clustering_client.get(
"/clusters/{cluster_id}/policies/{policy_id}".format(
cluster_id=name_or_id, policy_id=policy_name_or_id),
error_message="Error fetching policy "
"{name}".format(name=policy_name_or_id))
return self._get_and_munchify('cluster_policy', policy)
except Exception:
return False
def list_policies_on_cluster(self, name_or_id):
endpoint = "/clusters/{cluster_id}/policies".format(
cluster_id=name_or_id)
try:
data = self._clustering_client.get(
endpoint,
error_message="Error fetching cluster policies")
except exc.OpenStackCloudURINotFound as e:
self.log.debug(str(e), exc_info=True)
return []
return self._get_and_munchify('cluster_policies', data)
def create_cluster_profile(self, name, spec, metadata=None):
profile = {
'name': name,
'spec': spec
}
if metadata is not None:
profile['metadata'] = metadata
data = self._clustering_client.post(
'/profiles', json={'profile': profile},
error_message="Error creating profile {name}".format(name=name))
return self._get_and_munchify('profile', data)
def set_cluster_profile_metadata(self, name_or_id, metadata):
profile = self.get_cluster_profile(name_or_id)
if not profile:
raise exc.OpenStackCloudException(
'Invalid Profile {profile}'.format(profile=name_or_id))
self._clustering_client.post(
'/profiles/{profile_id}/metadata'.format(profile_id=profile['id']),
json={'metadata': metadata},
error_message='Error updating profile metadata')
def search_cluster_profiles(self, name_or_id=None, filters=None):
cluster_profiles = self.list_cluster_profiles()
return _utils._filter_list(cluster_profiles, name_or_id, filters)
def list_cluster_profiles(self):
try:
data = self._clustering_client.get(
'/profiles',
error_message="Error fetching profiles")
except exc.OpenStackCloudURINotFound as e:
self.log.debug(str(e), exc_info=True)
return []
return self._get_and_munchify('profiles', data)
def get_cluster_profile_by_id(self, profile_id):
try:
data = self._clustering_client.get(
"/profiles/{profile_id}".format(profile_id=profile_id),
error_message="Error fetching profile {name}".format(
name=profile_id))
return self._get_and_munchify('profile', data)
except exc.OpenStackCloudURINotFound as e:
self.log.debug(str(e), exc_info=True)
return None
def get_cluster_profile(self, name_or_id, filters=None):
return _utils._get_entity(self, 'cluster_profile', name_or_id, filters)
def delete_cluster_profile(self, name_or_id):
profile = self.get_cluster_profile(name_or_id)
if profile is None:
self.log.debug("Profile %s not found for deleting", name_or_id)
return False
for cluster in self.list_clusters():
if (name_or_id, profile.id) in cluster.items():
self.log.debug(
"Profile %s is being used by cluster %s, won't delete",
name_or_id, cluster.name)
return False
self._clustering_client.delete(
"/profiles/{profile_id}".format(profile_id=profile['id']),
error_message="Error deleting profile "
"{name}".format(name=name_or_id))
return True
def update_cluster_profile(self, name_or_id, metadata=None, new_name=None):
old_profile = self.get_cluster_profile(name_or_id)
if not old_profile:
raise exc.OpenStackCloudException(
'Invalid Profile {profile}'.format(profile=name_or_id))
profile = {}
if metadata is not None:
profile['metadata'] = metadata
if new_name is not None:
profile['name'] = new_name
data = self._clustering_client.patch(
"/profiles/{profile_id}".format(profile_id=old_profile.id),
json={'profile': profile},
error_message="Error updating profile {name}".format(
name=name_or_id))
return self._get_and_munchify(key=None, data=data)
def create_cluster_policy(self, name, spec):
policy = {
'name': name,
'spec': spec
}
data = self._clustering_client.post(
'/policies', json={'policy': policy},
error_message="Error creating policy {name}".format(
name=policy['name']))
return self._get_and_munchify('policy', data)
def search_cluster_policies(self, name_or_id=None, filters=None):
cluster_policies = self.list_cluster_policies()
return _utils._filter_list(cluster_policies, name_or_id, filters)
def list_cluster_policies(self):
endpoint = "/policies"
try:
data = self._clustering_client.get(
endpoint,
error_message="Error fetching cluster policies")
except exc.OpenStackCloudURINotFound as e:
self.log.debug(str(e), exc_info=True)
return []
return self._get_and_munchify('policies', data)
def get_cluster_policy_by_id(self, policy_id):
try:
data = self._clustering_client.get(
"/policies/{policy_id}".format(policy_id=policy_id),
error_message="Error fetching policy {name}".format(
name=policy_id))
return self._get_and_munchify('policy', data)
except exc.OpenStackCloudURINotFound as e:
self.log.debug(str(e), exc_info=True)
return None
def get_cluster_policy(self, name_or_id, filters=None):
return _utils._get_entity(
self, 'cluster_policie', name_or_id, filters)
def delete_cluster_policy(self, name_or_id):
policy = self.get_cluster_policy_by_id(name_or_id)
if policy is None:
self.log.debug("Policy %s not found for deleting", name_or_id)
return False
for cluster in self.list_clusters():
if (name_or_id, policy.id) in cluster.items():
self.log.debug(
"Policy %s is being used by cluster %s, won't delete",
name_or_id, cluster.name)
return False
self._clustering_client.delete(
"/policies/{policy_id}".format(policy_id=name_or_id),
error_message="Error deleting policy "
"{name}".format(name=name_or_id))
return True
def update_cluster_policy(self, name_or_id, new_name):
old_policy = self.get_cluster_policy(name_or_id)
if not old_policy:
raise exc.OpenStackCloudException(
'Invalid Policy {policy}'.format(policy=name_or_id))
policy = {'name': new_name}
data = self._clustering_client.patch(
"/policies/{policy_id}".format(policy_id=old_policy.id),
json={'policy': policy},
error_message="Error updating policy "
"{name}".format(name=name_or_id))
return self._get_and_munchify(key=None, data=data)
def create_cluster_receiver(self, name, receiver_type,
cluster_name_or_id=None, action=None,
actor=None, params=None):
cluster = self.get_cluster(cluster_name_or_id)
if cluster is None:
raise exc.OpenStackCloudException(
'Invalid cluster {cluster}'.format(cluster=cluster_name_or_id))
receiver = {
'name': name,
'type': receiver_type
}
if cluster_name_or_id is not None:
receiver['cluster_id'] = cluster.id
if action is not None:
receiver['action'] = action
if actor is not None:
receiver['actor'] = actor
if params is not None:
receiver['params'] = params
data = self._clustering_client.post(
'/receivers', json={'receiver': receiver},
error_message="Error creating receiver {name}".format(name=name))
return self._get_and_munchify('receiver', data)
def search_cluster_receivers(self, name_or_id=None, filters=None):
cluster_receivers = self.list_cluster_receivers()
return _utils._filter_list(cluster_receivers, name_or_id, filters)
def list_cluster_receivers(self):
try:
data = self._clustering_client.get(
'/receivers',
error_message="Error fetching receivers")
except exc.OpenStackCloudURINotFound as e:
self.log.debug(str(e), exc_info=True)
return []
return self._get_and_munchify('receivers', data)
def get_cluster_receiver_by_id(self, receiver_id):
try:
data = self._clustering_client.get(
"/receivers/{receiver_id}".format(receiver_id=receiver_id),
error_message="Error fetching receiver {name}".format(
name=receiver_id))
return self._get_and_munchify('receiver', data)
except exc.OpenStackCloudURINotFound as e:
self.log.debug(str(e), exc_info=True)
return None
def get_cluster_receiver(self, name_or_id, filters=None):
return _utils._get_entity(
self, 'cluster_receiver', name_or_id, filters)
def delete_cluster_receiver(self, name_or_id, wait=False, timeout=3600):
receiver = self.get_cluster_receiver(name_or_id)
if receiver is None:
self.log.debug("Receiver %s not found for deleting", name_or_id)
return False
receiver_id = receiver['id']
self._clustering_client.delete(
"/receivers/{receiver_id}".format(receiver_id=receiver_id),
error_message="Error deleting receiver {name}".format(
name=name_or_id))
if not wait:
return True
for count in utils.iterate_timeout(
timeout, "Timeout waiting for cluster receiver to delete"):
receiver = self.get_cluster_receiver_by_id(receiver_id)
if not receiver:
break
return True
def update_cluster_receiver(self, name_or_id, new_name=None, action=None,
params=None):
old_receiver = self.get_cluster_receiver(name_or_id)
if old_receiver is None:
raise exc.OpenStackCloudException(
'Invalid receiver {receiver}'.format(receiver=name_or_id))
receiver = {}
if new_name is not None:
receiver['name'] = new_name
if action is not None:
receiver['action'] = action
if params is not None:
receiver['params'] = params
data = self._clustering_client.patch(
"/receivers/{receiver_id}".format(receiver_id=old_receiver.id),
json={'receiver': receiver},
error_message="Error updating receiver {name}".format(
name=name_or_id))
return self._get_and_munchify(key=None, data=data)

425
openstack/cloud/_coe.py Normal file
View File

@ -0,0 +1,425 @@
# 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.
# import types so that we can reference ListType in sphinx param declarations.
# We can't just use list, because sphinx gets confused by
# openstack.resource.Resource.list and openstack.resource2.Resource.list
import types # noqa
from openstack.cloud import exc
from openstack.cloud import _normalize
from openstack.cloud import _utils
class CoeCloudMixin(_normalize.Normalizer):
@property
def _container_infra_client(self):
if 'container-infra' not in self._raw_clients:
self._raw_clients['container-infra'] = self._get_raw_client(
'container-infra')
return self._raw_clients['container-infra']
@_utils.cache_on_arguments()
def list_coe_clusters(self):
"""List COE(Ccontainer Orchestration Engine) cluster.
:returns: a list of dicts containing the cluster.
:raises: ``OpenStackCloudException``: if something goes wrong during
the OpenStack API call.
"""
with _utils.shade_exceptions("Error fetching cluster list"):
data = self._container_infra_client.get('/clusters')
return self._normalize_coe_clusters(
self._get_and_munchify('clusters', data))
def search_coe_clusters(
self, name_or_id=None, filters=None):
"""Search COE cluster.
:param name_or_id: cluster name or ID.
:param filters: a dict containing additional filters to use.
:param detail: a boolean to control if we need summarized or
detailed output.
:returns: a list of dict containing the cluster
:raises: ``OpenStackCloudException``: if something goes wrong during
the OpenStack API call.
"""
coe_clusters = self.list_coe_clusters()
return _utils._filter_list(
coe_clusters, name_or_id, filters)
def get_coe_cluster(self, name_or_id, filters=None):
"""Get a COE cluster by name or ID.
:param name_or_id: Name or ID of the cluster.
:param filters:
A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example::
{
'last_name': 'Smith',
'other': {
'gender': 'Female'
}
}
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A cluster dict or None if no matching cluster is found.
"""
return _utils._get_entity(self, 'coe_cluster', name_or_id,
filters=filters)
def create_coe_cluster(
self, name, cluster_template_id, **kwargs):
"""Create a COE cluster based on given cluster template.
:param string name: Name of the cluster.
:param string image_id: ID of the cluster template to use.
Other arguments will be passed in kwargs.
:returns: a dict containing the cluster description
:raises: ``OpenStackCloudException`` if something goes wrong during
the OpenStack API call
"""
error_message = ("Error creating cluster of name"
" {cluster_name}".format(cluster_name=name))
with _utils.shade_exceptions(error_message):
body = kwargs.copy()
body['name'] = name
body['cluster_template_id'] = cluster_template_id
cluster = self._container_infra_client.post(
'/clusters', json=body)
self.list_coe_clusters.invalidate(self)
return cluster
def delete_coe_cluster(self, name_or_id):
"""Delete a COE cluster.
:param name_or_id: Name or unique ID of the cluster.
:returns: True if the delete succeeded, False if the
cluster was not found.
:raises: OpenStackCloudException on operation error.
"""
cluster = self.get_coe_cluster(name_or_id)
if not cluster:
self.log.debug(
"COE Cluster %(name_or_id)s does not exist",
{'name_or_id': name_or_id},
exc_info=True)
return False
with _utils.shade_exceptions("Error in deleting COE cluster"):
self._container_infra_client.delete(
'/clusters/{id}'.format(id=cluster['id']))
self.list_coe_clusters.invalidate(self)
return True
@_utils.valid_kwargs('node_count')
def update_coe_cluster(self, name_or_id, operation, **kwargs):
"""Update a COE cluster.
:param name_or_id: Name or ID of the COE cluster being updated.
:param operation: Operation to perform - add, remove, replace.
Other arguments will be passed with kwargs.
:returns: a dict representing the updated cluster.
:raises: OpenStackCloudException on operation error.
"""
self.list_coe_clusters.invalidate(self)
cluster = self.get_coe_cluster(name_or_id)
if not cluster:
raise exc.OpenStackCloudException(
"COE cluster %s not found." % name_or_id)
if operation not in ['add', 'replace', 'remove']:
raise TypeError(
"%s operation not in 'add', 'replace', 'remove'" % operation)
patches = _utils.generate_patches_from_kwargs(operation, **kwargs)
# No need to fire an API call if there is an empty patch
if not patches:
return cluster
with _utils.shade_exceptions(
"Error updating COE cluster {0}".format(name_or_id)):
self._container_infra_client.patch(
'/clusters/{id}'.format(id=cluster['id']),
json=patches)
new_cluster = self.get_coe_cluster(name_or_id)
return new_cluster
def get_coe_cluster_certificate(self, cluster_id):
"""Get details about the CA certificate for a cluster by name or ID.
:param cluster_id: ID of the cluster.
:returns: Details about the CA certificate for the given cluster.
"""
msg = ("Error fetching CA cert for the cluster {cluster_id}".format(
cluster_id=cluster_id))
url = "/certificates/{cluster_id}".format(cluster_id=cluster_id)
data = self._container_infra_client.get(url,
error_message=msg)
return self._get_and_munchify(key=None, data=data)
def sign_coe_cluster_certificate(self, cluster_id, csr):
"""Sign client key and generate the CA certificate for a cluster
:param cluster_id: UUID of the cluster.
:param csr: Certificate Signing Request (CSR) for authenticating
client key.The CSR will be used by Magnum to generate
a signed certificate that client will use to communicate
with the cluster.
:returns: a dict representing the signed certs.
:raises: OpenStackCloudException on operation error.
"""
error_message = ("Error signing certs for cluster"
" {cluster_id}".format(cluster_id=cluster_id))
with _utils.shade_exceptions(error_message):
body = {}
body['cluster_uuid'] = cluster_id
body['csr'] = csr
certs = self._container_infra_client.post(
'/certificates', json=body)
return self._get_and_munchify(key=None, data=certs)
@_utils.cache_on_arguments()
def list_cluster_templates(self, detail=False):
"""List cluster templates.
:param bool detail. Ignored. Included for backwards compat.
ClusterTemplates are always returned with full details.
:returns: a list of dicts containing the cluster template details.
:raises: ``OpenStackCloudException``: if something goes wrong during
the OpenStack API call.
"""
with _utils.shade_exceptions("Error fetching cluster template list"):
try:
data = self._container_infra_client.get('/clustertemplates')
# NOTE(flwang): Magnum adds /clustertemplates and /cluster
# to deprecate /baymodels and /bay since Newton release. So
# we're using a small tag to indicate if current
# cloud has those two new API endpoints.
self._container_infra_client._has_magnum_after_newton = True
return self._normalize_cluster_templates(
self._get_and_munchify('clustertemplates', data))
except exc.OpenStackCloudURINotFound:
data = self._container_infra_client.get('/baymodels/detail')
return self._normalize_cluster_templates(
self._get_and_munchify('baymodels', data))
list_baymodels = list_cluster_templates
list_coe_cluster_templates = list_cluster_templates
def search_cluster_templates(
self, name_or_id=None, filters=None, detail=False):
"""Search cluster templates.
:param name_or_id: cluster template name or ID.
:param filters: a dict containing additional filters to use.
:param detail: a boolean to control if we need summarized or
detailed output.
:returns: a list of dict containing the cluster templates
:raises: ``OpenStackCloudException``: if something goes wrong during
the OpenStack API call.
"""
cluster_templates = self.list_cluster_templates(detail=detail)
return _utils._filter_list(
cluster_templates, name_or_id, filters)
search_baymodels = search_cluster_templates
search_coe_cluster_templates = search_cluster_templates
def get_cluster_template(self, name_or_id, filters=None, detail=False):
"""Get a cluster template by name or ID.
:param name_or_id: Name or ID of the cluster template.
:param filters:
A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example::
{
'last_name': 'Smith',
'other': {
'gender': 'Female'
}
}
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A cluster template dict or None if no matching
cluster template is found.
"""
return _utils._get_entity(self, 'cluster_template', name_or_id,
filters=filters, detail=detail)
get_baymodel = get_cluster_template
get_coe_cluster_template = get_cluster_template
def create_cluster_template(
self, name, image_id=None, keypair_id=None, coe=None, **kwargs):
"""Create a cluster template.
:param string name: Name of the cluster template.
:param string image_id: Name or ID of the image to use.
:param string keypair_id: Name or ID of the keypair to use.
:param string coe: Name of the coe for the cluster template.
Other arguments will be passed in kwargs.
:returns: a dict containing the cluster template description
:raises: ``OpenStackCloudException`` if something goes wrong during
the OpenStack API call
"""
error_message = ("Error creating cluster template of name"
" {cluster_template_name}".format(
cluster_template_name=name))
with _utils.shade_exceptions(error_message):
body = kwargs.copy()
body['name'] = name
body['image_id'] = image_id
body['keypair_id'] = keypair_id
body['coe'] = coe
try:
cluster_template = self._container_infra_client.post(
'/clustertemplates', json=body)
self._container_infra_client._has_magnum_after_newton = True
except exc.OpenStackCloudURINotFound:
cluster_template = self._container_infra_client.post(
'/baymodels', json=body)
self.list_cluster_templates.invalidate(self)
return cluster_template
create_baymodel = create_cluster_template
create_coe_cluster_template = create_cluster_template
def delete_cluster_template(self, name_or_id):
"""Delete a cluster template.
:param name_or_id: Name or unique ID of the cluster template.
:returns: True if the delete succeeded, False if the
cluster template was not found.
:raises: OpenStackCloudException on operation error.
"""
cluster_template = self.get_cluster_template(name_or_id)
if not cluster_template:
self.log.debug(
"Cluster template %(name_or_id)s does not exist",
{'name_or_id': name_or_id},
exc_info=True)
return False
with _utils.shade_exceptions("Error in deleting cluster template"):
if getattr(self._container_infra_client,
'_has_magnum_after_newton', False):
self._container_infra_client.delete(
'/clustertemplates/{id}'.format(id=cluster_template['id']))
else:
self._container_infra_client.delete(
'/baymodels/{id}'.format(id=cluster_template['id']))
self.list_cluster_templates.invalidate(self)
return True
delete_baymodel = delete_cluster_template
delete_coe_cluster_template = delete_cluster_template
@_utils.valid_kwargs('name', 'image_id', 'flavor_id', 'master_flavor_id',
'keypair_id', 'external_network_id', 'fixed_network',
'dns_nameserver', 'docker_volume_size', 'labels',
'coe', 'http_proxy', 'https_proxy', 'no_proxy',
'network_driver', 'tls_disabled', 'public',
'registry_enabled', 'volume_driver')
def update_cluster_template(self, name_or_id, operation, **kwargs):
"""Update a cluster template.
:param name_or_id: Name or ID of the cluster template being updated.
:param operation: Operation to perform - add, remove, replace.
Other arguments will be passed with kwargs.
:returns: a dict representing the updated cluster template.
:raises: OpenStackCloudException on operation error.
"""
self.list_cluster_templates.invalidate(self)
cluster_template = self.get_cluster_template(name_or_id)
if not cluster_template:
raise exc.OpenStackCloudException(
"Cluster template %s not found." % name_or_id)
if operation not in ['add', 'replace', 'remove']:
raise TypeError(
"%s operation not in 'add', 'replace', 'remove'" % operation)
patches = _utils.generate_patches_from_kwargs(operation, **kwargs)
# No need to fire an API call if there is an empty patch
if not patches:
return cluster_template
with _utils.shade_exceptions(
"Error updating cluster template {0}".format(name_or_id)):
if getattr(self._container_infra_client,
'_has_magnum_after_newton', False):
self._container_infra_client.patch(
'/clustertemplates/{id}'.format(id=cluster_template['id']),
json=patches)
else:
self._container_infra_client.patch(
'/baymodels/{id}'.format(id=cluster_template['id']),
json=patches)
new_cluster_template = self.get_cluster_template(name_or_id)
return new_cluster_template
update_baymodel = update_cluster_template
update_coe_cluster_template = update_cluster_template
def list_magnum_services(self):
"""List all Magnum services.
:returns: a list of dicts containing the service details.
:raises: OpenStackCloudException on operation error.
"""
with _utils.shade_exceptions("Error fetching Magnum services list"):
data = self._container_infra_client.get('/mservices')
return self._normalize_magnum_services(
self._get_and_munchify('mservices', data))

1917
openstack/cloud/_compute.py Normal file

File diff suppressed because it is too large Load Diff

296
openstack/cloud/_dns.py Normal file
View File

@ -0,0 +1,296 @@
# 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.
# import types so that we can reference ListType in sphinx param declarations.
# We can't just use list, because sphinx gets confused by
# openstack.resource.Resource.list and openstack.resource2.Resource.list
import types # noqa
from openstack.cloud import exc
from openstack.cloud import _normalize
from openstack.cloud import _utils
class DnsCloudMixin(_normalize.Normalizer):
@property
def _dns_client(self):
if 'dns' not in self._raw_clients:
dns_client = self._get_versioned_client(
'dns', min_version=2, max_version='2.latest')
self._raw_clients['dns'] = dns_client
return self._raw_clients['dns']
def list_zones(self):
"""List all available zones.
:returns: A list of zones dicts.
"""
data = self._dns_client.get(
"/zones",
error_message="Error fetching zones list")
return self._get_and_munchify('zones', data)
def get_zone(self, name_or_id, filters=None):
"""Get a zone by name or ID.
:param name_or_id: Name or ID of the zone
:param filters:
A dictionary of meta data to use for further filtering
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A zone dict or None if no matching zone is found.
"""
return _utils._get_entity(self, 'zone', name_or_id, filters)
def search_zones(self, name_or_id=None, filters=None):
zones = self.list_zones()
return _utils._filter_list(zones, name_or_id, filters)
def create_zone(self, name, zone_type=None, email=None, description=None,
ttl=None, masters=None):
"""Create a new zone.
:param name: Name of the zone being created.
:param zone_type: Type of the zone (primary/secondary)
:param email: Email of the zone owner (only
applies if zone_type is primary)
:param description: Description of the zone
:param ttl: TTL (Time to live) value in seconds
:param masters: Master nameservers (only applies
if zone_type is secondary)
:returns: a dict representing the created zone.
:raises: OpenStackCloudException on operation error.
"""
# We capitalize in case the user passes time in lowercase, as
# designate call expects PRIMARY/SECONDARY
if zone_type is not None:
zone_type = zone_type.upper()
if zone_type not in ('PRIMARY', 'SECONDARY'):
raise exc.OpenStackCloudException(
"Invalid type %s, valid choices are PRIMARY or SECONDARY" %
zone_type)
zone = {
"name": name,
"email": email,
"description": description,
}
if ttl is not None:
zone["ttl"] = ttl
if zone_type is not None:
zone["type"] = zone_type
if masters is not None:
zone["masters"] = masters
data = self._dns_client.post(
"/zones", json=zone,
error_message="Unable to create zone {name}".format(name=name))
return self._get_and_munchify(key=None, data=data)
@_utils.valid_kwargs('email', 'description', 'ttl', 'masters')
def update_zone(self, name_or_id, **kwargs):
"""Update a zone.
:param name_or_id: Name or ID of the zone being updated.
:param email: Email of the zone owner (only
applies if zone_type is primary)
:param description: Description of the zone
:param ttl: TTL (Time to live) value in seconds
:param masters: Master nameservers (only applies
if zone_type is secondary)
:returns: a dict representing the updated zone.
:raises: OpenStackCloudException on operation error.
"""
zone = self.get_zone(name_or_id)
if not zone:
raise exc.OpenStackCloudException(
"Zone %s not found." % name_or_id)
data = self._dns_client.patch(
"/zones/{zone_id}".format(zone_id=zone['id']), json=kwargs,
error_message="Error updating zone {0}".format(name_or_id))
return self._get_and_munchify(key=None, data=data)
def delete_zone(self, name_or_id):
"""Delete a zone.
:param name_or_id: Name or ID of the zone being deleted.
:returns: True if delete succeeded, False otherwise.
:raises: OpenStackCloudException on operation error.
"""
zone = self.get_zone(name_or_id)
if zone is None:
self.log.debug("Zone %s not found for deleting", name_or_id)
return False
return self._dns_client.delete(
"/zones/{zone_id}".format(zone_id=zone['id']),
error_message="Error deleting zone {0}".format(name_or_id))
return True
def list_recordsets(self, zone):
"""List all available recordsets.
:param zone: Name or ID of the zone managing the recordset
:returns: A list of recordsets.
"""
zone_obj = self.get_zone(zone)
if zone_obj is None:
raise exc.OpenStackCloudException(
"Zone %s not found." % zone)
return self._dns_client.get(
"/zones/{zone_id}/recordsets".format(zone_id=zone_obj['id']),
error_message="Error fetching recordsets list")['recordsets']
def get_recordset(self, zone, name_or_id):
"""Get a recordset by name or ID.
:param zone: Name or ID of the zone managing the recordset
:param name_or_id: Name or ID of the recordset
:returns: A recordset dict or None if no matching recordset is
found.
"""
zone_obj = self.get_zone(zone)
if zone_obj is None:
raise exc.OpenStackCloudException(
"Zone %s not found." % zone)
try:
return self._dns_client.get(
"/zones/{zone_id}/recordsets/{recordset_id}".format(
zone_id=zone_obj['id'], recordset_id=name_or_id),
error_message="Error fetching recordset")
except Exception:
return None
def search_recordsets(self, zone, name_or_id=None, filters=None):
recordsets = self.list_recordsets(zone=zone)
return _utils._filter_list(recordsets, name_or_id, filters)
def create_recordset(self, zone, name, recordset_type, records,
description=None, ttl=None):
"""Create a recordset.
:param zone: Name or ID of the zone managing the recordset
:param name: Name of the recordset
:param recordset_type: Type of the recordset
:param records: List of the recordset definitions
:param description: Description of the recordset
:param ttl: TTL value of the recordset
:returns: a dict representing the created recordset.
:raises: OpenStackCloudException on operation error.
"""
zone_obj = self.get_zone(zone)
if zone_obj is None:
raise exc.OpenStackCloudException(
"Zone %s not found." % zone)
# We capitalize the type in case the user sends in lowercase
recordset_type = recordset_type.upper()
body = {
'name': name,
'type': recordset_type,
'records': records
}
if description:
body['description'] = description
if ttl:
body['ttl'] = ttl
return self._dns_client.post(
"/zones/{zone_id}/recordsets".format(zone_id=zone_obj['id']),
json=body,
error_message="Error creating recordset {name}".format(name=name))
@_utils.valid_kwargs('description', 'ttl', 'records')
def update_recordset(self, zone, name_or_id, **kwargs):
"""Update a recordset.
:param zone: Name or ID of the zone managing the recordset
:param name_or_id: Name or ID of the recordset being updated.
:param records: List of the recordset definitions
:param description: Description of the recordset
:param ttl: TTL (Time to live) value in seconds of the recordset
:returns: a dict representing the updated recordset.
:raises: OpenStackCloudException on operation error.
"""
zone_obj = self.get_zone(zone)
if zone_obj is None:
raise exc.OpenStackCloudException(
"Zone %s not found." % zone)
recordset_obj = self.get_recordset(zone, name_or_id)
if recordset_obj is None:
raise exc.OpenStackCloudException(
"Recordset %s not found." % name_or_id)
new_recordset = self._dns_client.put(
"/zones/{zone_id}/recordsets/{recordset_id}".format(
zone_id=zone_obj['id'], recordset_id=name_or_id), json=kwargs,
error_message="Error updating recordset {0}".format(name_or_id))
return new_recordset
def delete_recordset(self, zone, name_or_id):
"""Delete a recordset.
:param zone: Name or ID of the zone managing the recordset.
:param name_or_id: Name or ID of the recordset being deleted.
:returns: True if delete succeeded, False otherwise.
:raises: OpenStackCloudException on operation error.
"""
zone_obj = self.get_zone(zone)
if zone_obj is None:
self.log.debug("Zone %s not found for deleting", zone)
return False
recordset = self.get_recordset(zone_obj['id'], name_or_id)
if recordset is None:
self.log.debug("Recordset %s not found for deleting", name_or_id)
return False
self._dns_client.delete(
"/zones/{zone_id}/recordsets/{recordset_id}".format(
zone_id=zone_obj['id'], recordset_id=name_or_id),
error_message="Error deleting recordset {0}".format(name_or_id))
return True

File diff suppressed because it is too large Load Diff

1534
openstack/cloud/_identity.py Normal file

File diff suppressed because it is too large Load Diff

432
openstack/cloud/_image.py Normal file
View File

@ -0,0 +1,432 @@
# 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.
# import types so that we can reference ListType in sphinx param declarations.
# We can't just use list, because sphinx gets confused by
# openstack.resource.Resource.list and openstack.resource2.Resource.list
import types # noqa
import keystoneauth1.exceptions
from openstack.cloud import exc
from openstack.cloud import meta
from openstack.cloud import _normalize
from openstack.cloud import _utils
from openstack import proxy
from openstack import utils
def _no_pending_images(images):
"""If there are any images not in a steady state, don't cache"""
for image in images:
if image.status not in ('active', 'deleted', 'killed'):
return False
return True
class ImageCloudMixin(_normalize.Normalizer):
def __init__(self):
self.image_api_use_tasks = self.config.config['image_api_use_tasks']
@property
def _raw_image_client(self):
if 'raw-image' not in self._raw_clients:
image_client = self._get_raw_client('image')
self._raw_clients['raw-image'] = image_client
return self._raw_clients['raw-image']
@property
def _image_client(self):
if 'image' not in self._raw_clients:
self._raw_clients['image'] = self._get_versioned_client(
'image', min_version=1, max_version='2.latest')
return self._raw_clients['image']
def search_images(self, name_or_id=None, filters=None):
images = self.list_images()
return _utils._filter_list(images, name_or_id, filters)
@_utils.cache_on_arguments(should_cache_fn=_no_pending_images)
def list_images(self, filter_deleted=True, show_all=False):
"""Get available images.
:param filter_deleted: Control whether deleted images are returned.
:param show_all: Show all images, including images that are shared
but not accepted. (By default in glance v2 shared image that
have not been accepted are not shown) show_all will override the
value of filter_deleted to False.
:returns: A list of glance images.
"""
if show_all:
filter_deleted = False
# First, try to actually get images from glance, it's more efficient
images = []
params = {}
image_list = []
try:
if self._is_client_version('image', 2):
endpoint = '/images'
if show_all:
params['member_status'] = 'all'
else:
endpoint = '/images/detail'
response = self._image_client.get(endpoint, params=params)
except keystoneauth1.exceptions.catalog.EndpointNotFound:
# We didn't have glance, let's try nova
# If this doesn't work - we just let the exception propagate
response = proxy._json_response(
self.compute.get('/images/detail'))
while 'next' in response:
image_list.extend(meta.obj_list_to_munch(response['images']))
endpoint = response['next']
# next links from glance have the version prefix. If the catalog
# has a versioned endpoint, then we can't append the next link to
# it. Strip the absolute prefix (/v1/ or /v2/ to turn it into
# a proper relative link.
if endpoint.startswith('/v'):
endpoint = endpoint[4:]
response = self._image_client.get(endpoint)
if 'images' in response:
image_list.extend(meta.obj_list_to_munch(response['images']))
else:
image_list.extend(response)
for image in image_list:
# The cloud might return DELETED for invalid images.
# While that's cute and all, that's an implementation detail.
if not filter_deleted:
images.append(image)
elif image.status.lower() != 'deleted':
images.append(image)
return self._normalize_images(images)
def get_image(self, name_or_id, filters=None):
"""Get an image by name or ID.
:param name_or_id: Name or ID of the image.
:param filters:
A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example::
{
'last_name': 'Smith',
'other': {
'gender': 'Female'
}
}
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: An image ``munch.Munch`` or None if no matching image
is found
"""
return _utils._get_entity(self, 'image', name_or_id, filters)
def get_image_by_id(self, id):
""" Get a image by ID
:param id: ID of the image.
:returns: An image ``munch.Munch``.
"""
data = self._image_client.get(
'/images/{id}'.format(id=id),
error_message="Error getting image with ID {id}".format(id=id)
)
key = 'image' if 'image' in data else None
image = self._normalize_image(
self._get_and_munchify(key, data))
return image
def download_image(
self, name_or_id, output_path=None, output_file=None,
chunk_size=1024):
"""Download an image by name or ID
:param str name_or_id: Name or ID of the image.
:param output_path: the output path to write the image to. Either this
or output_file must be specified
:param output_file: a file object (or file-like object) to write the
image data to. Only write() will be called on this object. Either
this or output_path must be specified
:param int chunk_size: size in bytes to read from the wire and buffer
at one time. Defaults to 1024
:raises: OpenStackCloudException in the event download_image is called
without exactly one of either output_path or output_file
:raises: OpenStackCloudResourceNotFound if no images are found matching
the name or ID provided
"""
if output_path is None and output_file is None:
raise exc.OpenStackCloudException(
'No output specified, an output path or file object'
' is necessary to write the image data to')
elif output_path is not None and output_file is not None:
raise exc.OpenStackCloudException(
'Both an output path and file object were provided,'
' however only one can be used at once')
image = self.search_images(name_or_id)
if len(image) == 0:
raise exc.OpenStackCloudResourceNotFound(
"No images with name or ID %s were found" % name_or_id, None)
if self._is_client_version('image', 2):
endpoint = '/images/{id}/file'.format(id=image[0]['id'])
else:
endpoint = '/images/{id}'.format(id=image[0]['id'])
response = self._image_client.get(endpoint, stream=True)
with _utils.shade_exceptions("Unable to download image"):
if output_path:
with open(output_path, 'wb') as fd:
for chunk in response.iter_content(chunk_size=chunk_size):
fd.write(chunk)
return
elif output_file:
for chunk in response.iter_content(chunk_size=chunk_size):
output_file.write(chunk)
return
def get_image_exclude(self, name_or_id, exclude):
for image in self.search_images(name_or_id):
if exclude:
if exclude not in image.name:
return image
else:
return image
return None
def get_image_name(self, image_id, exclude=None):
image = self.get_image_exclude(image_id, exclude)
if image:
return image.name
return None
def get_image_id(self, image_name, exclude=None):
image = self.get_image_exclude(image_name, exclude)
if image:
return image.id
return None
def wait_for_image(self, image, timeout=3600):
image_id = image['id']
for count in utils.iterate_timeout(
timeout, "Timeout waiting for image to snapshot"):
self.list_images.invalidate(self)
image = self.get_image(image_id)
if not image:
continue
if image['status'] == 'active':
return image
elif image['status'] == 'error':
raise exc.OpenStackCloudException(
'Image {image} hit error state'.format(image=image_id))
def delete_image(
self, name_or_id, wait=False, timeout=3600, delete_objects=True):
"""Delete an existing image.
:param name_or_id: Name of the image to be deleted.
:param wait: If True, waits for image to be deleted.
:param timeout: Seconds to wait for image deletion. None is forever.
:param delete_objects: If True, also deletes uploaded swift objects.
:returns: True if delete succeeded, False otherwise.
:raises: OpenStackCloudException if there are problems deleting.
"""
image = self.get_image(name_or_id)
if not image:
return False
self._image_client.delete(
'/images/{id}'.format(id=image.id),
error_message="Error in deleting image")
self.list_images.invalidate(self)
# Task API means an image was uploaded to swift
if self.image_api_use_tasks and (
self._IMAGE_OBJECT_KEY in image
or self._SHADE_IMAGE_OBJECT_KEY in image):
(container, objname) = image.get(
self._IMAGE_OBJECT_KEY, image.get(
self._SHADE_IMAGE_OBJECT_KEY)).split('/', 1)
self.delete_object(container=container, name=objname)
if wait:
for count in utils.iterate_timeout(
timeout,
"Timeout waiting for the image to be deleted."):
self._get_cache(None).invalidate()
if self.get_image(image.id) is None:
break
return True
def create_image(
self, name, filename=None,
container=None,
md5=None, sha256=None,
disk_format=None, container_format=None,
disable_vendor_agent=True,
wait=False, timeout=3600,
allow_duplicates=False, meta=None, volume=None, **kwargs):
"""Upload an image.
:param str name: Name of the image to create. If it is a pathname
of an image, the name will be constructed from the
extensionless basename of the path.
:param str filename: The path to the file to upload, if needed.
(optional, defaults to None)
:param str container: Name of the container in swift where images
should be uploaded for import if the cloud
requires such a thing. (optiona, defaults to
'images')
:param str md5: md5 sum of the image file. If not given, an md5 will
be calculated.
:param str sha256: sha256 sum of the image file. If not given, an md5
will be calculated.
:param str disk_format: The disk format the image is in. (optional,
defaults to the os-client-config config value
for this cloud)
:param str container_format: The container format the image is in.
(optional, defaults to the
os-client-config config value for this
cloud)
:param bool disable_vendor_agent: Whether or not to append metadata
flags to the image to inform the
cloud in question to not expect a
vendor agent to be runing.
(optional, defaults to True)
:param bool wait: If true, waits for image to be created. Defaults to
true - however, be aware that one of the upload
methods is always synchronous.
:param timeout: Seconds to wait for image creation. None is forever.
:param allow_duplicates: If true, skips checks that enforce unique
image name. (optional, defaults to False)
:param meta: A dict of key/value pairs to use for metadata that
bypasses automatic type conversion.
:param volume: Name or ID or volume object of a volume to create an
image from. Mutually exclusive with (optional, defaults
to None)
Additional kwargs will be passed to the image creation as additional
metadata for the image and will have all values converted to string
except for min_disk, min_ram, size and virtual_size which will be
converted to int.
If you are sure you have all of your data types correct or have an
advanced need to be explicit, use meta. If you are just a normal
consumer, using kwargs is likely the right choice.
If a value is in meta and kwargs, meta wins.
:returns: A ``munch.Munch`` of the Image object
:raises: OpenStackCloudException if there are problems uploading
"""
if volume:
image = self.block_storage.create_image(
name=name, volume=volume,
allow_duplicates=allow_duplicates,
container_format=container_format, disk_format=disk_format,
wait=wait, timeout=timeout)
else:
image = self.image.create_image(
name, filename=filename,
container=container,
md5=sha256, sha256=sha256,
disk_format=disk_format, container_format=container_format,
disable_vendor_agent=disable_vendor_agent,
wait=wait, timeout=timeout,
allow_duplicates=allow_duplicates, meta=meta, **kwargs)
self._get_cache(None).invalidate()
if not wait:
return image
try:
for count in utils.iterate_timeout(
timeout,
"Timeout waiting for the image to finish."):
image_obj = self.get_image(image.id)
if image_obj and image_obj.status not in ('queued', 'saving'):
return image_obj
except exc.OpenStackCloudTimeout:
self.log.debug(
"Timeout waiting for image to become ready. Deleting.")
self.delete_image(image.id, wait=True)
raise
def update_image_properties(
self, image=None, name_or_id=None, meta=None, **properties):
image = image or name_or_id
return self.image.update_image_properties(
image=image, meta=meta, **properties)
def set_volume_quotas(self, name_or_id, **kwargs):
""" Set a volume quota in a project
:param name_or_id: project name or id
:param kwargs: key/value pairs of quota name and quota value
:raises: OpenStackCloudException if the resource to set the
quota does not exist.
"""
proj = self.get_project(name_or_id)
if not proj:
raise exc.OpenStackCloudException("project does not exist")
kwargs['tenant_id'] = proj.id
self._volume_client.put(
'/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id),
json={'quota_set': kwargs},
error_message="No valid quota or resource")
def get_volume_quotas(self, name_or_id):
""" Get volume quotas for a project
:param name_or_id: project name or id
:raises: OpenStackCloudException if it's not a valid project
:returns: Munch object with the quotas
"""
proj = self.get_project(name_or_id)
if not proj:
raise exc.OpenStackCloudException("project does not exist")
data = self._volume_client.get(
'/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id),
error_message="cinder client call failed")
return self._get_and_munchify('quota_set', data)
def delete_volume_quotas(self, name_or_id):
""" Delete volume quotas for a project
:param name_or_id: project name or id
:raises: OpenStackCloudException if it's not a valid project or the
cinder client call failed
:returns: dict with the quotas
"""
proj = self.get_project(name_or_id)
if not proj:
raise exc.OpenStackCloudException("project does not exist")
return self._volume_client.delete(
'/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id),
error_message="cinder client call failed")

2566
openstack/cloud/_network.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,370 @@
# 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.
# import types so that we can reference ListType in sphinx param declarations.
# We can't just use list, because sphinx gets confused by
# openstack.resource.Resource.list and openstack.resource2.Resource.list
import threading
import types # noqa
from openstack.cloud import exc
from openstack.cloud import _normalize
class NetworkCommonCloudMixin(_normalize.Normalizer):
"""Shared networking functions used by FloatingIP, Network, Compute classes
"""
def __init__(self):
self._external_ipv4_names = self.config.get_external_ipv4_networks()
self._internal_ipv4_names = self.config.get_internal_ipv4_networks()
self._external_ipv6_names = self.config.get_external_ipv6_networks()
self._internal_ipv6_names = self.config.get_internal_ipv6_networks()
self._nat_destination = self.config.get_nat_destination()
self._nat_source = self.config.get_nat_source()
self._default_network = self.config.get_default_network()
self._use_external_network = self.config.config.get(
'use_external_network', True)
self._use_internal_network = self.config.config.get(
'use_internal_network', True)
self._networks_lock = threading.Lock()
self._reset_network_caches()
def use_external_network(self):
return self._use_external_network
def use_internal_network(self):
return self._use_internal_network
def _reset_network_caches(self):
# Variables to prevent us from going through the network finding
# logic again if we've done it once. This is different from just
# the cached value, since "None" is a valid value to find.
with self._networks_lock:
self._external_ipv4_networks = []
self._external_ipv4_floating_networks = []
self._internal_ipv4_networks = []
self._external_ipv6_networks = []
self._internal_ipv6_networks = []
self._nat_destination_network = None
self._nat_source_network = None
self._default_network_network = None
self._network_list_stamp = False
def _set_interesting_networks(self):
external_ipv4_networks = []
external_ipv4_floating_networks = []
internal_ipv4_networks = []
external_ipv6_networks = []
internal_ipv6_networks = []
nat_destination = None
nat_source = None
default_network = None
all_subnets = None
# Filter locally because we have an or condition
try:
# TODO(mordred): Rackspace exposes neutron but it does not
# work. I think that overriding what the service catalog
# reports should be a thing os-client-config should handle
# in a vendor profile - but for now it does not. That means
# this search_networks can just totally fail. If it does
# though, that's fine, clearly the neutron introspection is
# not going to work.
all_networks = self.list_networks()
except exc.OpenStackCloudException:
self._network_list_stamp = True
return
for network in all_networks:
# External IPv4 networks
if (network['name'] in self._external_ipv4_names
or network['id'] in self._external_ipv4_names):
external_ipv4_networks.append(network)
elif ((('router:external' in network
and network['router:external'])
or network.get('provider:physical_network'))
and network['name'] not in self._internal_ipv4_names
and network['id'] not in self._internal_ipv4_names):
external_ipv4_networks.append(network)
# Internal networks
if (network['name'] in self._internal_ipv4_names
or network['id'] in self._internal_ipv4_names):
internal_ipv4_networks.append(network)
elif (not network.get('router:external', False)
and not network.get('provider:physical_network')
and network['name'] not in self._external_ipv4_names
and network['id'] not in self._external_ipv4_names):
internal_ipv4_networks.append(network)
# External networks
if (network['name'] in self._external_ipv6_names
or network['id'] in self._external_ipv6_names):
external_ipv6_networks.append(network)
elif (network.get('router:external')
and network['name'] not in self._internal_ipv6_names
and network['id'] not in self._internal_ipv6_names):
external_ipv6_networks.append(network)
# Internal networks
if (network['name'] in self._internal_ipv6_names
or network['id'] in self._internal_ipv6_names):
internal_ipv6_networks.append(network)
elif (not network.get('router:external', False)
and network['name'] not in self._external_ipv6_names
and network['id'] not in self._external_ipv6_names):
internal_ipv6_networks.append(network)
# External Floating IPv4 networks
if self._nat_source in (
network['name'], network['id']):
if nat_source:
raise exc.OpenStackCloudException(
'Multiple networks were found matching'
' {nat_net} which is the network configured'
' to be the NAT source. Please check your'
' cloud resources. It is probably a good idea'
' to configure this network by ID rather than'
' by name.'.format(
nat_net=self._nat_source))
external_ipv4_floating_networks.append(network)
nat_source = network
elif self._nat_source is None:
if network.get('router:external'):
external_ipv4_floating_networks.append(network)
nat_source = nat_source or network
# NAT Destination
if self._nat_destination in (
network['name'], network['id']):
if nat_destination:
raise exc.OpenStackCloudException(
'Multiple networks were found matching'
' {nat_net} which is the network configured'
' to be the NAT destination. Please check your'
' cloud resources. It is probably a good idea'
' to configure this network by ID rather than'
' by name.'.format(
nat_net=self._nat_destination))
nat_destination = network
elif self._nat_destination is None:
# TODO(mordred) need a config value for floating
# ips for this cloud so that we can skip this
# No configured nat destination, we have to figured
# it out.
if all_subnets is None:
try:
all_subnets = self.list_subnets()
except exc.OpenStackCloudException:
# Thanks Rackspace broken neutron
all_subnets = []
for subnet in all_subnets:
# TODO(mordred) trap for detecting more than
# one network with a gateway_ip without a config
if ('gateway_ip' in subnet and subnet['gateway_ip']
and network['id'] == subnet['network_id']):
nat_destination = network
break
# Default network
if self._default_network in (
network['name'], network['id']):
if default_network:
raise exc.OpenStackCloudException(
'Multiple networks were found matching'
' {default_net} which is the network'
' configured to be the default interface'
' network. Please check your cloud resources.'
' It is probably a good idea'
' to configure this network by ID rather than'
' by name.'.format(
default_net=self._default_network))
default_network = network
# Validate config vs. reality
for net_name in self._external_ipv4_names:
if net_name not in [net['name'] for net in external_ipv4_networks]:
raise exc.OpenStackCloudException(
"Networks: {network} was provided for external IPv4"
" access and those networks could not be found".format(
network=net_name))
for net_name in self._internal_ipv4_names:
if net_name not in [net['name'] for net in internal_ipv4_networks]:
raise exc.OpenStackCloudException(
"Networks: {network} was provided for internal IPv4"
" access and those networks could not be found".format(
network=net_name))
for net_name in self._external_ipv6_names:
if net_name not in [net['name'] for net in external_ipv6_networks]:
raise exc.OpenStackCloudException(
"Networks: {network} was provided for external IPv6"
" access and those networks could not be found".format(
network=net_name))
for net_name in self._internal_ipv6_names:
if net_name not in [net['name'] for net in internal_ipv6_networks]:
raise exc.OpenStackCloudException(
"Networks: {network} was provided for internal IPv6"
" access and those networks could not be found".format(
network=net_name))
if self._nat_destination and not nat_destination:
raise exc.OpenStackCloudException(
'Network {network} was configured to be the'
' destination for inbound NAT but it could not be'
' found'.format(
network=self._nat_destination))
if self._nat_source and not nat_source:
raise exc.OpenStackCloudException(
'Network {network} was configured to be the'
' source for inbound NAT but it could not be'
' found'.format(
network=self._nat_source))
if self._default_network and not default_network:
raise exc.OpenStackCloudException(
'Network {network} was configured to be the'
' default network interface but it could not be'
' found'.format(
network=self._default_network))
self._external_ipv4_networks = external_ipv4_networks
self._external_ipv4_floating_networks = external_ipv4_floating_networks
self._internal_ipv4_networks = internal_ipv4_networks
self._external_ipv6_networks = external_ipv6_networks
self._internal_ipv6_networks = internal_ipv6_networks
self._nat_destination_network = nat_destination
self._nat_source_network = nat_source
self._default_network_network = default_network
def _find_interesting_networks(self):
if self._networks_lock.acquire():
try:
if self._network_list_stamp:
return
if (not self._use_external_network
and not self._use_internal_network):
# Both have been flagged as skip - don't do a list
return
if not self.has_service('network'):
return
self._set_interesting_networks()
self._network_list_stamp = True
finally:
self._networks_lock.release()
# def get_nat_destination(self):
# """Return the network that is configured to be the NAT destination.
#
# :returns: A network dict if one is found
# """
# self._find_interesting_networks()
# return self._nat_destination_network
def get_nat_source(self):
"""Return the network that is configured to be the NAT destination.
:returns: A network dict if one is found
"""
self._find_interesting_networks()
return self._nat_source_network
def get_default_network(self):
"""Return the network that is configured to be the default interface.
:returns: A network dict if one is found
"""
self._find_interesting_networks()
return self._default_network_network
def get_nat_destination(self):
"""Return the network that is configured to be the NAT destination.
:returns: A network dict if one is found
"""
self._find_interesting_networks()
return self._nat_destination_network
def get_external_networks(self):
"""Return the networks that are configured to route northbound.
This should be avoided in favor of the specific ipv4/ipv6 method,
but is here for backwards compatibility.
:returns: A list of network ``munch.Munch`` if one is found
"""
self._find_interesting_networks()
return list(
set(self._external_ipv4_networks)
| set(self._external_ipv6_networks))
def get_internal_networks(self):
"""Return the networks that are configured to not route northbound.
This should be avoided in favor of the specific ipv4/ipv6 method,
but is here for backwards compatibility.
:returns: A list of network ``munch.Munch`` if one is found
"""
self._find_interesting_networks()
return list(
set(self._internal_ipv4_networks)
| set(self._internal_ipv6_networks))
def get_external_ipv4_networks(self):
"""Return the networks that are configured to route northbound.
:returns: A list of network ``munch.Munch`` if one is found
"""
self._find_interesting_networks()
return self._external_ipv4_networks
def get_external_ipv4_floating_networks(self):
"""Return the networks that are configured to route northbound.
:returns: A list of network ``munch.Munch`` if one is found
"""
self._find_interesting_networks()
return self._external_ipv4_floating_networks
def get_internal_ipv4_networks(self):
"""Return the networks that are configured to not route northbound.
:returns: A list of network ``munch.Munch`` if one is found
"""
self._find_interesting_networks()
return self._internal_ipv4_networks
def get_external_ipv6_networks(self):
"""Return the networks that are configured to route northbound.
:returns: A list of network ``munch.Munch`` if one is found
"""
self._find_interesting_networks()
return self._external_ipv6_networks
def get_internal_ipv6_networks(self):
"""Return the networks that are configured to not route northbound.
:returns: A list of network ``munch.Munch`` if one is found
"""
self._find_interesting_networks()
return self._internal_ipv6_networks

View File

@ -0,0 +1,837 @@
# 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.
# import types so that we can reference ListType in sphinx param declarations.
# We can't just use list, because sphinx gets confused by
# openstack.resource.Resource.list and openstack.resource2.Resource.list
import collections
import concurrent.futures
import hashlib
import json
import os
import six
import types # noqa
from six.moves import urllib
import keystoneauth1.exceptions
from openstack.cloud import exc
from openstack.cloud import _normalize
from openstack.cloud import _utils
from openstack import exceptions
from openstack import proxy
DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB
# This halves the current default for Swift
DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2
OBJECT_CONTAINER_ACLS = {
'public': '.r:*,.rlistings',
'private': '',
}
class ObjectStoreCloudMixin(_normalize.Normalizer):
def __init__(self):
self.__pool_executor = None
@property
def _object_store_client(self):
if 'object-store' not in self._raw_clients:
raw_client = self._get_raw_client('object-store')
self._raw_clients['object-store'] = raw_client
return self._raw_clients['object-store']
@property
def _pool_executor(self):
if not self.__pool_executor:
# TODO(mordred) Make this configurable - and probably use Futurist
# instead of concurrent.futures so that people using Eventlet will
# be happier.
self.__pool_executor = concurrent.futures.ThreadPoolExecutor(
max_workers=5)
return self.__pool_executor
def list_containers(self, full_listing=True, prefix=None):
"""List containers.
:param full_listing: Ignored. Present for backwards compat
:returns: list of Munch of the container objects
:raises: OpenStackCloudException on operation error.
"""
params = dict(format='json', prefix=prefix)
response = self.object_store.get('/', params=params)
return self._get_and_munchify(None, proxy._json_response(response))
def search_containers(self, name=None, filters=None):
"""Search containers.
:param string name: container name.
:param filters: a dict containing additional filters to use.
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: a list of ``munch.Munch`` containing the containers.
:raises: ``OpenStackCloudException``: if something goes wrong during
the OpenStack API call.
"""
containers = self.list_containers()
return _utils._filter_list(containers, name, filters)
def get_container(self, name, skip_cache=False):
"""Get metadata about a container.
:param str name:
Name of the container to get metadata for.
:param bool skip_cache:
Ignore the cache of container metadata for this container.o
Defaults to ``False``.
"""
if skip_cache or name not in self._container_cache:
try:
response = self.object_store.head(name)
exceptions.raise_from_response(response)
self._container_cache[name] = response.headers
except exc.OpenStackCloudHTTPError as e:
if e.response.status_code == 404:
return None
raise
return self._container_cache[name]
def create_container(self, name, public=False):
"""Create an object-store container.
:param str name:
Name of the container to create.
:param bool public:
Whether to set this container to be public. Defaults to ``False``.
"""
container = self.get_container(name)
if container:
return container
exceptions.raise_from_response(self.object_store.put(name))
if public:
self.set_container_access(name, 'public')
return self.get_container(name, skip_cache=True)
def delete_container(self, name):
"""Delete an object-store container.
:param str name: Name of the container to delete.
"""
try:
exceptions.raise_from_response(self.object_store.delete(name))
self._container_cache.pop(name, None)
return True
except exc.OpenStackCloudHTTPError as e:
if e.response.status_code == 404:
return False
if e.response.status_code == 409:
raise exc.OpenStackCloudException(
'Attempt to delete container {container} failed. The'
' container is not empty. Please delete the objects'
' inside it before deleting the container'.format(
container=name))
raise
def update_container(self, name, headers):
"""Update the metadata in a container.
:param str name:
Name of the container to create.
:param dict headers:
Key/Value headers to set on the container.
"""
"""Update the metadata in a container.
:param str name:
Name of the container to update.
:param dict headers:
Key/Value headers to set on the container.
"""
exceptions.raise_from_response(
self.object_store.post(name, headers=headers))
def set_container_access(self, name, access):
"""Set the access control list on a container.
:param str name:
Name of the container.
:param str access:
ACL string to set on the container. Can also be ``public``
or ``private`` which will be translated into appropriate ACL
strings.
"""
if access not in OBJECT_CONTAINER_ACLS:
raise exc.OpenStackCloudException(
"Invalid container access specified: %s. Must be one of %s"
% (access, list(OBJECT_CONTAINER_ACLS.keys())))
header = {'x-container-read': OBJECT_CONTAINER_ACLS[access]}
self.update_container(name, header)
def get_container_access(self, name):
"""Get the control list from a container.
:param str name: Name of the container.
"""
container = self.get_container(name, skip_cache=True)
if not container:
raise exc.OpenStackCloudException("Container not found: %s" % name)
acl = container.get('x-container-read', '')
for key, value in OBJECT_CONTAINER_ACLS.items():
# Convert to string for the comparison because swiftclient
# returns byte values as bytes sometimes and apparently ==
# on bytes doesn't work like you'd think
if str(acl) == str(value):
return key
raise exc.OpenStackCloudException(
"Could not determine container access for ACL: %s." % acl)
def _get_file_hashes(self, filename):
file_key = "{filename}:{mtime}".format(
filename=filename,
mtime=os.stat(filename).st_mtime)
if file_key not in self._file_hash_cache:
self.log.debug(
'Calculating hashes for %(filename)s', {'filename': filename})
md5 = hashlib.md5()
sha256 = hashlib.sha256()
with open(filename, 'rb') as file_obj:
for chunk in iter(lambda: file_obj.read(8192), b''):
md5.update(chunk)
sha256.update(chunk)
self._file_hash_cache[file_key] = dict(
md5=md5.hexdigest(), sha256=sha256.hexdigest())
self.log.debug(
"Image file %(filename)s md5:%(md5)s sha256:%(sha256)s",
{'filename': filename,
'md5': self._file_hash_cache[file_key]['md5'],
'sha256': self._file_hash_cache[file_key]['sha256']})
return (self._file_hash_cache[file_key]['md5'],
self._file_hash_cache[file_key]['sha256'])
@_utils.cache_on_arguments()
def get_object_capabilities(self):
"""Get infomation about the object-storage service
The object-storage service publishes a set of capabilities that
include metadata about maximum values and thresholds.
"""
# The endpoint in the catalog has version and project-id in it
# To get capabilities, we have to disassemble and reassemble the URL
# This logic is taken from swiftclient
endpoint = urllib.parse.urlparse(self.object_store.get_endpoint())
url = "{scheme}://{netloc}/info".format(
scheme=endpoint.scheme, netloc=endpoint.netloc)
return proxy._json_response(self.object_store.get(url))
def get_object_segment_size(self, segment_size):
"""Get a segment size that will work given capabilities"""
if segment_size is None:
segment_size = DEFAULT_OBJECT_SEGMENT_SIZE
min_segment_size = 0
try:
caps = self.get_object_capabilities()
except exc.OpenStackCloudHTTPError as e:
if e.response.status_code in (404, 412):
# Clear the exception so that it doesn't linger
# and get reported as an Inner Exception later
_utils._exc_clear()
server_max_file_size = DEFAULT_MAX_FILE_SIZE
self.log.info(
"Swift capabilities not supported. "
"Using default max file size.")
else:
raise
else:
server_max_file_size = caps.get('swift', {}).get('max_file_size',
0)
min_segment_size = caps.get('slo', {}).get('min_segment_size', 0)
if segment_size > server_max_file_size:
return server_max_file_size
if segment_size < min_segment_size:
return min_segment_size
return segment_size
def is_object_stale(
self, container, name, filename, file_md5=None, file_sha256=None):
"""Check to see if an object matches the hashes of a file.
:param container: Name of the container.
:param name: Name of the object.
:param filename: Path to the file.
:param file_md5:
Pre-calculated md5 of the file contents. Defaults to None which
means calculate locally.
:param file_sha256:
Pre-calculated sha256 of the file contents. Defaults to None which
means calculate locally.
"""
metadata = self.get_object_metadata(container, name)
if not metadata:
self.log.debug(
"swift stale check, no object: {container}/{name}".format(
container=container, name=name))
return True
if not (file_md5 or file_sha256):
(file_md5, file_sha256) = self._get_file_hashes(filename)
md5_key = metadata.get(
self._OBJECT_MD5_KEY, metadata.get(self._SHADE_OBJECT_MD5_KEY, ''))
sha256_key = metadata.get(
self._OBJECT_SHA256_KEY, metadata.get(
self._SHADE_OBJECT_SHA256_KEY, ''))
up_to_date = self._hashes_up_to_date(
md5=file_md5, sha256=file_sha256,
md5_key=md5_key, sha256_key=sha256_key)
if not up_to_date:
self.log.debug(
"swift checksum mismatch: "
" %(filename)s!=%(container)s/%(name)s",
{'filename': filename, 'container': container, 'name': name})
return True
self.log.debug(
"swift object up to date: %(container)s/%(name)s",
{'container': container, 'name': name})
return False
def create_directory_marker_object(self, container, name, **headers):
"""Create a zero-byte directory marker object
.. note::
This method is not needed in most cases. Modern swift does not
require directory marker objects. However, some swift installs may
need these.
When using swift Static Web and Web Listings to serve static content
one may need to create a zero-byte object to represent each
"directory". Doing so allows Web Listings to generate an index of the
objects inside of it, and allows Static Web to render index.html
"files" that are "inside" the directory.
:param container: The name of the container.
:param name: Name for the directory marker object within the container.
:param headers: These will be passed through to the object creation
API as HTTP Headers.
"""
headers['content-type'] = 'application/directory'
return self.create_object(
container,
name,
data='',
generate_checksums=False,
**headers)
def create_object(
self, container, name, filename=None,
md5=None, sha256=None, segment_size=None,
use_slo=True, metadata=None,
generate_checksums=None, data=None,
**headers):
"""Create a file object.
Automatically uses large-object segments if needed.
:param container: The name of the container to store the file in.
This container will be created if it does not exist already.
:param name: Name for the object within the container.
:param filename: The path to the local file whose contents will be
uploaded. Mutually exclusive with data.
:param data: The content to upload to the object. Mutually exclusive
with filename.
:param md5: A hexadecimal md5 of the file. (Optional), if it is known
and can be passed here, it will save repeating the expensive md5
process. It is assumed to be accurate.
:param sha256: A hexadecimal sha256 of the file. (Optional) See md5.
:param segment_size: Break the uploaded object into segments of this
many bytes. (Optional) Shade will attempt to discover the maximum
value for this from the server if it is not specified, or will use
a reasonable default.
:param headers: These will be passed through to the object creation
API as HTTP Headers.
:param use_slo: If the object is large enough to need to be a Large
Object, use a static rather than dynamic object. Static Objects
will delete segment objects when the manifest object is deleted.
(optional, defaults to True)
:param generate_checksums: Whether to generate checksums on the client
side that get added to headers for later prevention of double
uploads of identical data. (optional, defaults to True)
:param metadata: This dict will get changed into headers that set
metadata of the object
:raises: ``OpenStackCloudException`` on operation error.
"""
if data is not None and filename:
raise ValueError(
"Both filename and data given. Please choose one.")
if data is not None and not name:
raise ValueError(
"name is a required parameter when data is given")
if data is not None and generate_checksums:
raise ValueError(
"checksums cannot be generated with data parameter")
if generate_checksums is None:
if data is not None:
generate_checksums = False
else:
generate_checksums = True
if not metadata:
metadata = {}
if not filename and data is None:
filename = name
if generate_checksums and (md5 is None or sha256 is None):
(md5, sha256) = self._get_file_hashes(filename)
if md5:
headers[self._OBJECT_MD5_KEY] = md5 or ''
if sha256:
headers[self._OBJECT_SHA256_KEY] = sha256 or ''
for (k, v) in metadata.items():
headers['x-object-meta-' + k] = v
endpoint = '{container}/{name}'.format(container=container, name=name)
if data is not None:
self.log.debug(
"swift uploading data to %(endpoint)s",
{'endpoint': endpoint})
return self._upload_object_data(endpoint, data, headers)
# segment_size gets used as a step value in a range call, so needs
# to be an int
if segment_size:
segment_size = int(segment_size)
segment_size = self.get_object_segment_size(segment_size)
file_size = os.path.getsize(filename)
if self.is_object_stale(container, name, filename, md5, sha256):
self.log.debug(
"swift uploading %(filename)s to %(endpoint)s",
{'filename': filename, 'endpoint': endpoint})
if file_size <= segment_size:
self._upload_object(endpoint, filename, headers)
else:
self._upload_large_object(
endpoint, filename, headers,
file_size, segment_size, use_slo)
def _upload_object_data(self, endpoint, data, headers):
return proxy._json_response(self.object_store.put(
endpoint, headers=headers, data=data))
def _upload_object(self, endpoint, filename, headers):
return proxy._json_response(self.object_store.put(
endpoint, headers=headers, data=open(filename, 'rb')))
def _get_file_segments(self, endpoint, filename, file_size, segment_size):
# Use an ordered dict here so that testing can replicate things
segments = collections.OrderedDict()
for (index, offset) in enumerate(range(0, file_size, segment_size)):
remaining = file_size - (index * segment_size)
segment = _utils.FileSegment(
filename, offset,
segment_size if segment_size < remaining else remaining)
name = '{endpoint}/{index:0>6}'.format(
endpoint=endpoint, index=index)
segments[name] = segment
return segments
def _object_name_from_url(self, url):
'''Get container_name/object_name from the full URL called.
Remove the Swift endpoint from the front of the URL, and remove
the leaving / that will leave behind.'''
endpoint = self.object_store.get_endpoint()
object_name = url.replace(endpoint, '')
if object_name.startswith('/'):
object_name = object_name[1:]
return object_name
def _add_etag_to_manifest(self, segment_results, manifest):
for result in segment_results:
if 'Etag' not in result.headers:
continue
name = self._object_name_from_url(result.url)
for entry in manifest:
if entry['path'] == '/{name}'.format(name=name):
entry['etag'] = result.headers['Etag']
def _upload_large_object(
self, endpoint, filename,
headers, file_size, segment_size, use_slo):
# If the object is big, we need to break it up into segments that
# are no larger than segment_size, upload each of them individually
# and then upload a manifest object. The segments can be uploaded in
# parallel, so we'll use the async feature of the TaskManager.
segment_futures = []
segment_results = []
retry_results = []
retry_futures = []
manifest = []
# Get an OrderedDict with keys being the swift location for the
# segment, the value a FileSegment file-like object that is a
# slice of the data for the segment.
segments = self._get_file_segments(
endpoint, filename, file_size, segment_size)
# Schedule the segments for upload
for name, segment in segments.items():
# Async call to put - schedules execution and returns a future
segment_future = self._pool_executor.submit(
self.object_store.put,
name, headers=headers, data=segment,
raise_exc=False)
segment_futures.append(segment_future)
# TODO(mordred) Collect etags from results to add to this manifest
# dict. Then sort the list of dicts by path.
manifest.append(dict(
path='/{name}'.format(name=name),
size_bytes=segment.length))
# Try once and collect failed results to retry
segment_results, retry_results = self._wait_for_futures(
segment_futures, raise_on_error=False)
self._add_etag_to_manifest(segment_results, manifest)
for result in retry_results:
# Grab the FileSegment for the failed upload so we can retry
name = self._object_name_from_url(result.url)
segment = segments[name]
segment.seek(0)
# Async call to put - schedules execution and returns a future
segment_future = self._pool_executor.submit(
self.object_store.put,
name, headers=headers, data=segment)
# TODO(mordred) Collect etags from results to add to this manifest
# dict. Then sort the list of dicts by path.
retry_futures.append(segment_future)
# If any segments fail the second time, just throw the error
segment_results, retry_results = self._wait_for_futures(
retry_futures, raise_on_error=True)
self._add_etag_to_manifest(segment_results, manifest)
if use_slo:
return self._finish_large_object_slo(endpoint, headers, manifest)
else:
return self._finish_large_object_dlo(endpoint, headers)
def _finish_large_object_slo(self, endpoint, headers, manifest):
# TODO(mordred) send an etag of the manifest, which is the md5sum
# of the concatenation of the etags of the results
headers = headers.copy()
return self._object_store_client.put(
endpoint,
params={'multipart-manifest': 'put'},
headers=headers, data=json.dumps(manifest))
def _finish_large_object_dlo(self, endpoint, headers):
headers = headers.copy()
headers['X-Object-Manifest'] = endpoint
return self._object_store_client.put(endpoint, headers=headers)
def update_object(self, container, name, metadata=None, **headers):
"""Update the metadata of an object
:param container: The name of the container the object is in
:param name: Name for the object within the container.
:param metadata: This dict will get changed into headers that set
metadata of the object
:param headers: These will be passed through to the object update
API as HTTP Headers.
:raises: ``OpenStackCloudException`` on operation error.
"""
if not metadata:
metadata = {}
metadata_headers = {}
for (k, v) in metadata.items():
metadata_headers['x-object-meta-' + k] = v
headers = dict(headers, **metadata_headers)
return self._object_store_client.post(
'{container}/{object}'.format(
container=container, object=name),
headers=headers)
def list_objects(self, container, full_listing=True, prefix=None):
"""List objects.
:param container: Name of the container to list objects in.
:param full_listing: Ignored. Present for backwards compat
:param string prefix:
only objects with this prefix will be returned.
(optional)
:returns: list of Munch of the objects
:raises: OpenStackCloudException on operation error.
"""
params = dict(format='json', prefix=prefix)
data = self._object_store_client.get(container, params=params)
return self._get_and_munchify(None, data)
def search_objects(self, container, name=None, filters=None):
"""Search objects.
:param string name: object name.
:param filters: a dict containing additional filters to use.
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: a list of ``munch.Munch`` containing the objects.
:raises: ``OpenStackCloudException``: if something goes wrong during
the OpenStack API call.
"""
objects = self.list_objects(container)
return _utils._filter_list(objects, name, filters)
def delete_object(self, container, name, meta=None):
"""Delete an object from a container.
:param string container: Name of the container holding the object.
:param string name: Name of the object to delete.
:param dict meta: Metadata for the object in question. (optional, will
be fetched if not provided)
:returns: True if delete succeeded, False if the object was not found.
:raises: OpenStackCloudException on operation error.
"""
# TODO(mordred) DELETE for swift returns status in text/plain format
# like so:
# Number Deleted: 15
# Number Not Found: 0
# Response Body:
# Response Status: 200 OK
# Errors:
# We should ultimately do something with that
try:
if not meta:
meta = self.get_object_metadata(container, name)
if not meta:
return False
params = {}
if meta.get('X-Static-Large-Object', None) == 'True':
params['multipart-manifest'] = 'delete'
self._object_store_client.delete(
'{container}/{object}'.format(
container=container, object=name),
params=params)
return True
except exc.OpenStackCloudHTTPError:
return False
def delete_autocreated_image_objects(self, container=None):
"""Delete all objects autocreated for image uploads.
This method should generally not be needed, as shade should clean up
the objects it uses for object-based image creation. If something
goes wrong and it is found that there are leaked objects, this method
can be used to delete any objects that shade has created on the user's
behalf in service of image uploads.
"""
if container is None:
container = self._OBJECT_AUTOCREATE_CONTAINER
# This method only makes sense on clouds that use tasks
if not self.image_api_use_tasks:
return False
deleted = False
for obj in self.list_objects(container):
meta = self.get_object_metadata(container, obj['name'])
if meta.get(
self._OBJECT_AUTOCREATE_KEY, meta.get(
self._SHADE_OBJECT_AUTOCREATE_KEY)) == 'true':
if self.delete_object(container, obj['name'], meta):
deleted = True
return deleted
def get_object_metadata(self, container, name):
try:
return self._object_store_client.head(
'{container}/{object}'.format(
container=container, object=name)).headers
except exc.OpenStackCloudException as e:
if e.response.status_code == 404:
return None
raise
def get_object_raw(self, container, obj, query_string=None, stream=False):
"""Get a raw response object for an object.
:param string container: name of the container.
:param string obj: name of the object.
:param string query_string:
query args for uri. (delimiter, prefix, etc.)
:param bool stream:
Whether to stream the response or not.
:returns: A `requests.Response`
:raises: OpenStackCloudException on operation error.
"""
endpoint = self._get_object_endpoint(container, obj, query_string)
return self._object_store_client.get(endpoint, stream=stream)
def _get_object_endpoint(self, container, obj, query_string):
endpoint = '{container}/{object}'.format(
container=container, object=obj)
if query_string:
endpoint = '{endpoint}?{query_string}'.format(
endpoint=endpoint, query_string=query_string)
return endpoint
def stream_object(
self, container, obj, query_string=None, resp_chunk_size=1024):
"""Download the content via a streaming iterator.
:param string container: name of the container.
:param string obj: name of the object.
:param string query_string:
query args for uri. (delimiter, prefix, etc.)
:param int resp_chunk_size:
chunk size of data to read. Only used if the results are
:returns:
An iterator over the content or None if the object is not found.
:raises: OpenStackCloudException on operation error.
"""
try:
with self.get_object_raw(
container, obj, query_string=query_string) as response:
for ret in response.iter_content(chunk_size=resp_chunk_size):
yield ret
except exc.OpenStackCloudHTTPError as e:
if e.response.status_code == 404:
return
raise
def get_object(self, container, obj, query_string=None,
resp_chunk_size=1024, outfile=None, stream=False):
"""Get the headers and body of an object
:param string container: name of the container.
:param string obj: name of the object.
:param string query_string:
query args for uri. (delimiter, prefix, etc.)
:param int resp_chunk_size:
chunk size of data to read. Only used if the results are
being written to a file or stream is True.
(optional, defaults to 1k)
:param outfile:
Write the object to a file instead of returning the contents.
If this option is given, body in the return tuple will be None.
outfile can either be a file path given as a string, or a
File like object.
:returns: Tuple (headers, body) of the object, or None if the object
is not found (404).
:raises: OpenStackCloudException on operation error.
"""
# TODO(mordred) implement resp_chunk_size
endpoint = self._get_object_endpoint(container, obj, query_string)
try:
get_stream = (outfile is not None)
with self._object_store_client.get(
endpoint, stream=get_stream) as response:
response_headers = {
k.lower(): v for k, v in response.headers.items()}
if outfile:
if isinstance(outfile, six.string_types):
outfile_handle = open(outfile, 'wb')
else:
outfile_handle = outfile
for chunk in response.iter_content(
resp_chunk_size, decode_unicode=False):
outfile_handle.write(chunk)
if isinstance(outfile, six.string_types):
outfile_handle.close()
else:
outfile_handle.flush()
return (response_headers, None)
else:
return (response_headers, response.text)
except exc.OpenStackCloudHTTPError as e:
if e.response.status_code == 404:
return None
raise
def _wait_for_futures(self, futures, raise_on_error=True):
'''Collect results or failures from a list of running future tasks.'''
results = []
retries = []
# Check on each result as its thread finishes
for completed in concurrent.futures.as_completed(futures):
try:
result = completed.result()
exceptions.raise_from_response(result)
results.append(result)
except (keystoneauth1.exceptions.RetriableConnectionFailure,
exceptions.HttpException) as e:
error_text = "Exception processing async task: {}".format(
str(e))
if raise_on_error:
self.log.exception(error_text)
raise
else:
self.log.debug(error_text)
# If we get an exception, put the result into a list so we
# can try again
retries.append(completed.result())
return results, retries
def _hashes_up_to_date(self, md5, sha256, md5_key, sha256_key):
'''Compare md5 and sha256 hashes for being up to date
md5 and sha256 are the current values.
md5_key and sha256_key are the previous values.
'''
up_to_date = False
if md5 and md5_key == md5:
up_to_date = True
if sha256 and sha256_key == sha256:
up_to_date = True
if md5 and md5_key != md5:
up_to_date = False
if sha256 and sha256_key != sha256:
up_to_date = False
return up_to_date

View File

@ -0,0 +1,275 @@
# 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.
# import types so that we can reference ListType in sphinx param declarations.
# We can't just use list, because sphinx gets confused by
# openstack.resource.Resource.list and openstack.resource2.Resource.list
import types # noqa
from openstack.cloud import exc
from openstack.cloud._heat import event_utils
from openstack.cloud._heat import template_utils
from openstack.cloud import _normalize
from openstack.cloud import _utils
def _no_pending_stacks(stacks):
"""If there are any stacks not in a steady state, don't cache"""
for stack in stacks:
status = stack['stack_status']
if '_COMPLETE' not in status and '_FAILED' not in status:
return False
return True
class OrchestrationCloudMixin(_normalize.Normalizer):
@property
def _orchestration_client(self):
if 'orchestration' not in self._raw_clients:
raw_client = self._get_raw_client('orchestration')
self._raw_clients['orchestration'] = raw_client
return self._raw_clients['orchestration']
def get_template_contents(
self, template_file=None, template_url=None,
template_object=None, files=None):
try:
return template_utils.get_template_contents(
template_file=template_file, template_url=template_url,
template_object=template_object, files=files)
except Exception as e:
raise exc.OpenStackCloudException(
"Error in processing template files: %s" % str(e))
def create_stack(
self, name, tags=None,
template_file=None, template_url=None,
template_object=None, files=None,
rollback=True,
wait=False, timeout=3600,
environment_files=None,
**parameters):
"""Create a stack.
:param string name: Name of the stack.
:param tags: List of tag(s) of the stack. (optional)
:param string template_file: Path to the template.
:param string template_url: URL of template.
:param string template_object: URL to retrieve template object.
:param dict files: dict of additional file content to include.
:param boolean rollback: Enable rollback on create failure.
:param boolean wait: Whether to wait for the delete to finish.
:param int timeout: Stack create timeout in seconds.
:param environment_files: Paths to environment files to apply.
Other arguments will be passed as stack parameters which will take
precedence over any parameters specified in the environments.
Only one of template_file, template_url, template_object should be
specified.
:returns: a dict containing the stack description
:raises: ``OpenStackCloudException`` if something goes wrong during
the OpenStack API call
"""
envfiles, env = template_utils.process_multiple_environments_and_files(
env_paths=environment_files)
tpl_files, template = template_utils.get_template_contents(
template_file=template_file,
template_url=template_url,
template_object=template_object,
files=files)
params = dict(
stack_name=name,
tags=tags,
disable_rollback=not rollback,
parameters=parameters,
template=template,
files=dict(list(tpl_files.items()) + list(envfiles.items())),
environment=env,
timeout_mins=timeout // 60,
)
self._orchestration_client.post('/stacks', json=params)
if wait:
event_utils.poll_for_events(self, stack_name=name,
action='CREATE')
return self.get_stack(name)
def update_stack(
self, name_or_id,
template_file=None, template_url=None,
template_object=None, files=None,
rollback=True,
wait=False, timeout=3600,
environment_files=None,
**parameters):
"""Update a stack.
:param string name_or_id: Name or ID of the stack to update.
:param string template_file: Path to the template.
:param string template_url: URL of template.
:param string template_object: URL to retrieve template object.
:param dict files: dict of additional file content to include.
:param boolean rollback: Enable rollback on update failure.
:param boolean wait: Whether to wait for the delete to finish.
:param int timeout: Stack update timeout in seconds.
:param environment_files: Paths to environment files to apply.
Other arguments will be passed as stack parameters which will take
precedence over any parameters specified in the environments.
Only one of template_file, template_url, template_object should be
specified.
:returns: a dict containing the stack description
:raises: ``OpenStackCloudException`` if something goes wrong during
the OpenStack API calls
"""
envfiles, env = template_utils.process_multiple_environments_and_files(
env_paths=environment_files)
tpl_files, template = template_utils.get_template_contents(
template_file=template_file,
template_url=template_url,
template_object=template_object,
files=files)
params = dict(
disable_rollback=not rollback,
parameters=parameters,
template=template,
files=dict(list(tpl_files.items()) + list(envfiles.items())),
environment=env,
timeout_mins=timeout // 60,
)
if wait:
# find the last event to use as the marker
events = event_utils.get_events(
self, name_or_id, event_args={'sort_dir': 'desc', 'limit': 1})
marker = events[0].id if events else None
self._orchestration_client.put(
'/stacks/{name_or_id}'.format(name_or_id=name_or_id), json=params)
if wait:
event_utils.poll_for_events(self,
name_or_id,
action='UPDATE',
marker=marker)
return self.get_stack(name_or_id)
def delete_stack(self, name_or_id, wait=False):
"""Delete a stack
:param string name_or_id: Stack name or ID.
:param boolean wait: Whether to wait for the delete to finish
:returns: True if delete succeeded, False if the stack was not found.
:raises: ``OpenStackCloudException`` if something goes wrong during
the OpenStack API call
"""
stack = self.get_stack(name_or_id, resolve_outputs=False)
if stack is None:
self.log.debug("Stack %s not found for deleting", name_or_id)
return False
if wait:
# find the last event to use as the marker
events = event_utils.get_events(
self, name_or_id, event_args={'sort_dir': 'desc', 'limit': 1})
marker = events[0].id if events else None
self._orchestration_client.delete(
'/stacks/{id}'.format(id=stack['id']))
if wait:
try:
event_utils.poll_for_events(self,
stack_name=name_or_id,
action='DELETE',
marker=marker)
except exc.OpenStackCloudHTTPError:
pass
stack = self.get_stack(name_or_id, resolve_outputs=False)
if stack and stack['stack_status'] == 'DELETE_FAILED':
raise exc.OpenStackCloudException(
"Failed to delete stack {id}: {reason}".format(
id=name_or_id, reason=stack['stack_status_reason']))
return True
def search_stacks(self, name_or_id=None, filters=None):
"""Search stacks.
:param name_or_id: Name or ID of the desired stack.
:param filters: a dict containing additional filters to use. e.g.
{'stack_status': 'CREATE_COMPLETE'}
:returns: a list of ``munch.Munch`` containing the stack description.
:raises: ``OpenStackCloudException`` if something goes wrong during the
OpenStack API call.
"""
stacks = self.list_stacks()
return _utils._filter_list(stacks, name_or_id, filters)
@_utils.cache_on_arguments(should_cache_fn=_no_pending_stacks)
def list_stacks(self):
"""List all stacks.
:returns: a list of ``munch.Munch`` containing the stack description.
:raises: ``OpenStackCloudException`` if something goes wrong during the
OpenStack API call.
"""
data = self._orchestration_client.get(
'/stacks', error_message="Error fetching stack list")
return self._normalize_stacks(
self._get_and_munchify('stacks', data))
def get_stack(self, name_or_id, filters=None, resolve_outputs=True):
"""Get exactly one stack.
:param name_or_id: Name or ID of the desired stack.
:param filters: a dict containing additional filters to use. e.g.
{'stack_status': 'CREATE_COMPLETE'}
:param resolve_outputs: If True, then outputs for this
stack will be resolved
:returns: a ``munch.Munch`` containing the stack description
:raises: ``OpenStackCloudException`` if something goes wrong during the
OpenStack API call or if multiple matches are found.
"""
def _search_one_stack(name_or_id=None, filters=None):
# stack names are mandatory and enforced unique in the project
# so a StackGet can always be used for name or ID.
try:
url = '/stacks/{name_or_id}'.format(name_or_id=name_or_id)
if not resolve_outputs:
url = '{url}?resolve_outputs=False'.format(url=url)
data = self._orchestration_client.get(
url,
error_message="Error fetching stack")
stack = self._get_and_munchify('stack', data)
# Treat DELETE_COMPLETE stacks as a NotFound
if stack['stack_status'] == 'DELETE_COMPLETE':
return []
except exc.OpenStackCloudURINotFound:
return []
stack = self._normalize_stack(stack)
return _utils._filter_list([stack], name_or_id, filters)
return _utils._get_entity(
self, _search_one_stack, name_or_id, filters)

View File

@ -0,0 +1,387 @@
# 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.
# import types so that we can reference ListType in sphinx param declarations.
# We can't just use list, because sphinx gets confused by
# openstack.resource.Resource.list and openstack.resource2.Resource.list
# import jsonpatch
import types # noqa
from openstack.cloud import exc
from openstack.cloud import _normalize
from openstack.cloud import _utils
from openstack import exceptions
from openstack import proxy
class SecurityGroupCloudMixin(_normalize.Normalizer):
def __init__(self):
self.secgroup_source = self.config.config['secgroup_source']
def get_security_group(self, name_or_id, filters=None):
"""Get a security group by name or ID.
:param name_or_id: Name or ID of the security group.
:param filters:
A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example::
{
'last_name': 'Smith',
'other': {
'gender': 'Female'
}
}
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A security group ``munch.Munch`` or None if no matching
security group is found.
"""
return _utils._get_entity(
self, 'security_group', name_or_id, filters)
def get_security_group_by_id(self, id):
""" Get a security group by ID
:param id: ID of the security group.
:returns: A security group ``munch.Munch``.
"""
if not self._has_secgroups():
raise exc.OpenStackCloudUnavailableFeature(
"Unavailable feature: security groups"
)
error_message = ("Error getting security group with"
" ID {id}".format(id=id))
if self._use_neutron_secgroups():
resp = self.network.get('/security-groups/{id}'.format(id=id))
data = proxy._json_response(resp, error_message=error_message)
else:
data = proxy._json_response(
self.compute.get(
'/os-security-groups/{id}'.format(id=id)),
error_message=error_message)
return self._normalize_secgroup(
self._get_and_munchify('security_group', data))
def create_security_group(self, name, description, project_id=None):
"""Create a new security group
:param string name: A name for the security group.
:param string description: Describes the security group.
:param string project_id:
Specify the project ID this security group will be created
on (admin-only).
:returns: A ``munch.Munch`` representing the new security group.
:raises: OpenStackCloudException on operation error.
:raises: OpenStackCloudUnavailableFeature if security groups are
not supported on this cloud.
"""
# Security groups not supported
if not self._has_secgroups():
raise exc.OpenStackCloudUnavailableFeature(
"Unavailable feature: security groups"
)
data = []
security_group_json = {
'security_group': {
'name': name, 'description': description
}}
if project_id is not None:
security_group_json['security_group']['tenant_id'] = project_id
if self._use_neutron_secgroups():
data = proxy._json_response(
self.network.post(
'/security-groups.json',
json=security_group_json),
error_message="Error creating security group {0}".format(name))
else:
data = proxy._json_response(self.compute.post(
'/os-security-groups', json=security_group_json))
return self._normalize_secgroup(
self._get_and_munchify('security_group', data))
def delete_security_group(self, name_or_id):
"""Delete a security group
:param string name_or_id: The name or unique ID of the security group.
:returns: True if delete succeeded, False otherwise.
:raises: OpenStackCloudException on operation error.
:raises: OpenStackCloudUnavailableFeature if security groups are
not supported on this cloud.
"""
# Security groups not supported
if not self._has_secgroups():
raise exc.OpenStackCloudUnavailableFeature(
"Unavailable feature: security groups"
)
# TODO(mordred): Let's come back and stop doing a GET before we do
# the delete.
secgroup = self.get_security_group(name_or_id)
if secgroup is None:
self.log.debug('Security group %s not found for deleting',
name_or_id)
return False
if self._use_neutron_secgroups():
exceptions.raise_from_response(
self.network.delete(
'/security-groups/{sg_id}.json'.format(
sg_id=secgroup['id'])),
error_message="Error deleting security group {0}".format(
name_or_id)
)
return True
else:
proxy._json_response(self.compute.delete(
'/os-security-groups/{id}'.format(id=secgroup['id'])))
return True
@_utils.valid_kwargs('name', 'description')
def update_security_group(self, name_or_id, **kwargs):
"""Update a security group
:param string name_or_id: Name or ID of the security group to update.
:param string name: New name for the security group.
:param string description: New description for the security group.
:returns: A ``munch.Munch`` describing the updated security group.
:raises: OpenStackCloudException on operation error.
"""
# Security groups not supported
if not self._has_secgroups():
raise exc.OpenStackCloudUnavailableFeature(
"Unavailable feature: security groups"
)
group = self.get_security_group(name_or_id)
if group is None:
raise exc.OpenStackCloudException(
"Security group %s not found." % name_or_id)
if self._use_neutron_secgroups():
data = proxy._json_response(
self.network.put(
'/security-groups/{sg_id}.json'.format(sg_id=group['id']),
json={'security_group': kwargs}),
error_message="Error updating security group {0}".format(
name_or_id))
else:
for key in ('name', 'description'):
kwargs.setdefault(key, group[key])
data = proxy._json_response(
self.compute.put(
'/os-security-groups/{id}'.format(id=group['id']),
json={'security_group': kwargs}))
return self._normalize_secgroup(
self._get_and_munchify('security_group', data))
def create_security_group_rule(self,
secgroup_name_or_id,
port_range_min=None,
port_range_max=None,
protocol=None,
remote_ip_prefix=None,
remote_group_id=None,
direction='ingress',
ethertype='IPv4',
project_id=None):
"""Create a new security group rule
:param string secgroup_name_or_id:
The security group name or ID to associate with this security
group rule. If a non-unique group name is given, an exception
is raised.
:param int port_range_min:
The minimum port number in the range that is matched by the
security group rule. If the protocol is TCP or UDP, this value
must be less than or equal to the port_range_max attribute value.
If nova is used by the cloud provider for security groups, then
a value of None will be transformed to -1.
:param int port_range_max:
The maximum port number in the range that is matched by the
security group rule. The port_range_min attribute constrains the
port_range_max attribute. If nova is used by the cloud provider
for security groups, then a value of None will be transformed
to -1.
:param string protocol:
The protocol that is matched by the security group rule. Valid
values are None, tcp, udp, and icmp.
:param string remote_ip_prefix:
The remote IP prefix to be associated with this security group
rule. This attribute matches the specified IP prefix as the
source IP address of the IP packet.
:param string remote_group_id:
The remote group ID to be associated with this security group
rule.
:param string direction:
Ingress or egress: The direction in which the security group
rule is applied. For a compute instance, an ingress security
group rule is applied to incoming (ingress) traffic for that
instance. An egress rule is applied to traffic leaving the
instance.
:param string ethertype:
Must be IPv4 or IPv6, and addresses represented in CIDR must
match the ingress or egress rules.
:param string project_id:
Specify the project ID this security group will be created
on (admin-only).
:returns: A ``munch.Munch`` representing the new security group rule.
:raises: OpenStackCloudException on operation error.
"""
# Security groups not supported
if not self._has_secgroups():
raise exc.OpenStackCloudUnavailableFeature(
"Unavailable feature: security groups"
)
secgroup = self.get_security_group(secgroup_name_or_id)
if not secgroup:
raise exc.OpenStackCloudException(
"Security group %s not found." % secgroup_name_or_id)
if self._use_neutron_secgroups():
# NOTE: Nova accepts -1 port numbers, but Neutron accepts None
# as the equivalent value.
rule_def = {
'security_group_id': secgroup['id'],
'port_range_min':
None if port_range_min == -1 else port_range_min,
'port_range_max':
None if port_range_max == -1 else port_range_max,
'protocol': protocol,
'remote_ip_prefix': remote_ip_prefix,
'remote_group_id': remote_group_id,
'direction': direction,
'ethertype': ethertype
}
if project_id is not None:
rule_def['tenant_id'] = project_id
data = proxy._json_response(
self.network.post(
'/security-group-rules.json',
json={'security_group_rule': rule_def}),
error_message="Error creating security group rule")
else:
# NOTE: Neutron accepts None for protocol. Nova does not.
if protocol is None:
raise exc.OpenStackCloudException('Protocol must be specified')
if direction == 'egress':
self.log.debug(
'Rule creation failed: Nova does not support egress rules'
)
raise exc.OpenStackCloudException(
'No support for egress rules')
# NOTE: Neutron accepts None for ports, but Nova requires -1
# as the equivalent value for ICMP.
#
# For TCP/UDP, if both are None, Neutron allows this and Nova
# represents this as all ports (1-65535). Nova does not accept
# None values, so to hide this difference, we will automatically
# convert to the full port range. If only a single port value is
# specified, it will error as normal.
if protocol == 'icmp':
if port_range_min is None:
port_range_min = -1
if port_range_max is None:
port_range_max = -1
elif protocol in ['tcp', 'udp']:
if port_range_min is None and port_range_max is None:
port_range_min = 1
port_range_max = 65535
security_group_rule_dict = dict(security_group_rule=dict(
parent_group_id=secgroup['id'],
ip_protocol=protocol,
from_port=port_range_min,
to_port=port_range_max,
cidr=remote_ip_prefix,
group_id=remote_group_id
))
if project_id is not None:
security_group_rule_dict[
'security_group_rule']['tenant_id'] = project_id
data = proxy._json_response(
self.compute.post(
'/os-security-group-rules',
json=security_group_rule_dict
))
return self._normalize_secgroup_rule(
self._get_and_munchify('security_group_rule', data))
def delete_security_group_rule(self, rule_id):
"""Delete a security group rule
:param string rule_id: The unique ID of the security group rule.
:returns: True if delete succeeded, False otherwise.
:raises: OpenStackCloudException on operation error.
:raises: OpenStackCloudUnavailableFeature if security groups are
not supported on this cloud.
"""
# Security groups not supported
if not self._has_secgroups():
raise exc.OpenStackCloudUnavailableFeature(
"Unavailable feature: security groups"
)
if self._use_neutron_secgroups():
try:
exceptions.raise_from_response(
self.network.delete(
'/security-group-rules/{sg_id}.json'.format(
sg_id=rule_id)),
error_message="Error deleting security group rule "
"{0}".format(rule_id))
except exc.OpenStackCloudResourceNotFound:
return False
return True
else:
try:
exceptions.raise_from_response(
self.compute.delete(
'/os-security-group-rules/{id}'.format(id=rule_id)))
except exc.OpenStackCloudResourceNotFound:
return False
return True
def _has_secgroups(self):
if not self.secgroup_source:
return False
else:
return self.secgroup_source.lower() in ('nova', 'neutron')
def _use_neutron_secgroups(self):
return (self.has_service('network')
and self.secgroup_source == 'neutron')

File diff suppressed because it is too large Load Diff

View File

@ -164,6 +164,20 @@ import six
from openstack import _log
from openstack._meta import connection as _meta
from openstack.cloud import openstackcloud as _cloud
from openstack.cloud import _baremetal
from openstack.cloud import _block_storage
from openstack.cloud import _compute
from openstack.cloud import _clustering
from openstack.cloud import _coe
from openstack.cloud import _dns
from openstack.cloud import _floating_ip
from openstack.cloud import _identity
from openstack.cloud import _image
from openstack.cloud import _network
from openstack.cloud import _network_common
from openstack.cloud import _object_store
from openstack.cloud import _orchestration
from openstack.cloud import _security_group
from openstack import config as _config
from openstack.config import cloud_region
from openstack import exceptions
@ -211,7 +225,22 @@ def from_config(cloud=None, config=None, options=None, **kwargs):
class Connection(six.with_metaclass(_meta.ConnectionMeta,
_cloud._OpenStackCloudMixin)):
_cloud._OpenStackCloudMixin,
_baremetal.BaremetalCloudMixin,
_block_storage.BlockStorageCloudMixin,
_compute.ComputeCloudMixin,
_clustering.ClusteringCloudMixin,
_coe.CoeCloudMixin,
_dns.DnsCloudMixin,
_floating_ip.FloatingIPCloudMixin,
_identity.IdentityCloudMixin,
_image.ImageCloudMixin,
_network.NetworkCloudMixin,
_network_common.NetworkCommonCloudMixin,
_object_store.ObjectStoreCloudMixin,
_orchestration.OrchestrationCloudMixin,
_security_group.SecurityGroupCloudMixin
)):
def __init__(self, cloud=None, config=None, session=None,
app_name=None, app_version=None,
@ -298,9 +327,23 @@ class Connection(six.with_metaclass(_meta.ConnectionMeta,
self._proxies = {}
self.use_direct_get = use_direct_get
self.strict_mode = strict
# Call the _OpenStackCloudMixin constructor while we work on
# Call the _*CloudMixin constructors while we work on
# integrating things better.
_cloud._OpenStackCloudMixin.__init__(self)
_baremetal.BaremetalCloudMixin.__init__(self)
_block_storage.BlockStorageCloudMixin.__init__(self)
_clustering.ClusteringCloudMixin.__init__(self)
_coe.CoeCloudMixin.__init__(self)
_compute.ComputeCloudMixin.__init__(self)
_dns.DnsCloudMixin.__init__(self)
_floating_ip.FloatingIPCloudMixin.__init__(self)
_identity.IdentityCloudMixin.__init__(self)
_image.ImageCloudMixin.__init__(self)
_network_common.NetworkCommonCloudMixin.__init__(self)
_network.NetworkCloudMixin.__init__(self)
_object_store.ObjectStoreCloudMixin.__init__(self)
_orchestration.OrchestrationCloudMixin.__init__(self)
_security_group.SecurityGroupCloudMixin.__init__(self)
@property
def session(self):

View File

@ -15,7 +15,6 @@
# import unittest
from openstack import connection
from openstack.cloud.openstackcloud import _OpenStackCloudMixin
from openstack.tests.functional import base
HYPERVISORS = []
@ -25,7 +24,7 @@ def hypervisors():
global HYPERVISORS
if HYPERVISORS:
return True
HYPERVISORS = _OpenStackCloudMixin.list_hypervisors(
HYPERVISORS = connection.Connection.list_hypervisors(
connection.from_config(cloud_name=base.TEST_CLOUD_NAME))
return bool(HYPERVISORS)

View File

@ -21,7 +21,7 @@ import uuid
import mock
from openstack.cloud import openstackcloud
from openstack import connection
from openstack.cloud import exc
from openstack.cloud import meta
from openstack.tests import fakes
@ -325,7 +325,7 @@ class TestCreateServer(base.TestCase):
self.assert_calls()
@mock.patch.object(openstackcloud._OpenStackCloudMixin, "wait_for_server")
@mock.patch.object(connection.Connection, "wait_for_server")
def test_create_server_with_admin_pass_wait(self, mock_wait):
"""
Test that a server with an admin_pass passed returns the password
@ -411,9 +411,8 @@ class TestCreateServer(base.TestCase):
self.assert_calls()
@mock.patch.object(
openstackcloud._OpenStackCloudMixin, "get_active_server")
@mock.patch.object(openstackcloud._OpenStackCloudMixin, "get_server")
@mock.patch.object(connection.Connection, "get_active_server")
@mock.patch.object(connection.Connection, "get_server")
def test_wait_for_server(self, mock_get_server, mock_get_active_server):
"""
Test that waiting for a server returns the server instance when
@ -447,7 +446,7 @@ class TestCreateServer(base.TestCase):
self.assertEqual('ACTIVE', server['status'])
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'wait_for_server')
@mock.patch.object(connection.Connection, 'wait_for_server')
def test_create_server_wait(self, mock_wait):
"""
Test that create_server with a wait actually does the wait.
@ -484,8 +483,7 @@ class TestCreateServer(base.TestCase):
)
self.assert_calls()
@mock.patch.object(
openstackcloud._OpenStackCloudMixin, 'add_ips_to_server')
@mock.patch.object(connection.Connection, 'add_ips_to_server')
def test_create_server_no_addresses(
self, mock_add_ips_to_server):
"""

View File

@ -21,17 +21,17 @@ Tests floating IP resource methods for Neutron and Nova-network.
from mock import patch
from openstack import connection
from openstack.cloud import meta
from openstack.cloud import openstackcloud
from openstack.tests import fakes
from openstack.tests.unit import base
class TestFloatingIP(base.TestCase):
@patch.object(openstackcloud._OpenStackCloudMixin, 'get_floating_ip')
@patch.object(openstackcloud._OpenStackCloudMixin, '_attach_ip_to_server')
@patch.object(openstackcloud._OpenStackCloudMixin, 'available_floating_ip')
@patch.object(connection.Connection, 'get_floating_ip')
@patch.object(connection.Connection, '_attach_ip_to_server')
@patch.object(connection.Connection, 'available_floating_ip')
def test_add_auto_ip(
self, mock_available_floating_ip, mock_attach_ip_to_server,
mock_get_floating_ip):
@ -57,7 +57,7 @@ class TestFloatingIP(base.TestCase):
timeout=60, wait=False, server=server_dict,
floating_ip=floating_ip_dict, skip_attach=False)
@patch.object(openstackcloud._OpenStackCloudMixin, '_add_ip_from_pool')
@patch.object(connection.Connection, '_add_ip_from_pool')
def test_add_ips_to_server_pool(self, mock_add_ip_from_pool):
server_dict = fakes.make_fake_server(
server_id='romeo', name='test-server', status="ACTIVE",
@ -70,9 +70,9 @@ class TestFloatingIP(base.TestCase):
server_dict, pool, reuse=True, wait=False, timeout=60,
fixed_address=None, nat_destination=None)
@patch.object(openstackcloud._OpenStackCloudMixin, 'has_service')
@patch.object(openstackcloud._OpenStackCloudMixin, 'get_floating_ip')
@patch.object(openstackcloud._OpenStackCloudMixin, '_add_auto_ip')
@patch.object(connection.Connection, 'has_service')
@patch.object(connection.Connection, 'get_floating_ip')
@patch.object(connection.Connection, '_add_auto_ip')
def test_add_ips_to_server_ipv6_only(
self, mock_add_auto_ip,
mock_get_floating_ip,
@ -109,9 +109,9 @@ class TestFloatingIP(base.TestCase):
self.assertEqual(
new_server['public_v6'], '2001:4800:7819:103:be76:4eff:fe05:8525')
@patch.object(openstackcloud._OpenStackCloudMixin, 'has_service')
@patch.object(openstackcloud._OpenStackCloudMixin, 'get_floating_ip')
@patch.object(openstackcloud._OpenStackCloudMixin, '_add_auto_ip')
@patch.object(connection.Connection, 'has_service')
@patch.object(connection.Connection, 'get_floating_ip')
@patch.object(connection.Connection, '_add_auto_ip')
def test_add_ips_to_server_rackspace(
self, mock_add_auto_ip,
mock_get_floating_ip,
@ -145,9 +145,9 @@ class TestFloatingIP(base.TestCase):
new_server['interface_ip'],
'2001:4800:7819:103:be76:4eff:fe05:8525')
@patch.object(openstackcloud._OpenStackCloudMixin, 'has_service')
@patch.object(openstackcloud._OpenStackCloudMixin, 'get_floating_ip')
@patch.object(openstackcloud._OpenStackCloudMixin, '_add_auto_ip')
@patch.object(connection.Connection, 'has_service')
@patch.object(connection.Connection, 'get_floating_ip')
@patch.object(connection.Connection, '_add_auto_ip')
def test_add_ips_to_server_rackspace_local_ipv4(
self, mock_add_auto_ip,
mock_get_floating_ip,
@ -179,7 +179,7 @@ class TestFloatingIP(base.TestCase):
mock_add_auto_ip.assert_not_called()
self.assertEqual(new_server['interface_ip'], '104.130.246.91')
@patch.object(openstackcloud._OpenStackCloudMixin, 'add_ip_list')
@patch.object(connection.Connection, 'add_ip_list')
def test_add_ips_to_server_ip_list(self, mock_add_ip_list):
server_dict = fakes.make_fake_server(
server_id='server-id', name='test-server', status="ACTIVE",
@ -191,8 +191,8 @@ class TestFloatingIP(base.TestCase):
mock_add_ip_list.assert_called_with(
server_dict, ips, wait=False, timeout=60, fixed_address=None)
@patch.object(openstackcloud._OpenStackCloudMixin, '_needs_floating_ip')
@patch.object(openstackcloud._OpenStackCloudMixin, '_add_auto_ip')
@patch.object(connection.Connection, '_needs_floating_ip')
@patch.object(connection.Connection, '_add_auto_ip')
def test_add_ips_to_server_auto_ip(
self, mock_add_auto_ip, mock_needs_floating_ip):
server_dict = fakes.make_fake_server(

View File

@ -14,7 +14,7 @@
import mock
from openstack.cloud import openstackcloud
from openstack import connection
from openstack.cloud import meta
from openstack.tests import fakes
from openstack.tests.unit import base
@ -353,10 +353,10 @@ class TestMeta(base.TestCase):
'10.0.0.101', meta.get_server_private_ip(srv, self.cloud))
self.assert_calls()
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'has_service')
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes')
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name')
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name')
@mock.patch.object(connection.Connection, 'has_service')
@mock.patch.object(connection.Connection, 'get_volumes')
@mock.patch.object(connection.Connection, 'get_image_name')
@mock.patch.object(connection.Connection, 'get_flavor_name')
def test_get_server_private_ip_devstack(
self,
mock_get_flavor_name, mock_get_image_name,
@ -418,9 +418,9 @@ class TestMeta(base.TestCase):
self.assertEqual(PRIVATE_V4, srv['private_v4'])
self.assert_calls()
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes')
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name')
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name')
@mock.patch.object(connection.Connection, 'get_volumes')
@mock.patch.object(connection.Connection, 'get_image_name')
@mock.patch.object(connection.Connection, 'get_flavor_name')
def test_get_server_private_ip_no_fip(
self,
mock_get_flavor_name, mock_get_image_name,
@ -468,9 +468,9 @@ class TestMeta(base.TestCase):
self.assertEqual(PRIVATE_V4, srv['private_v4'])
self.assert_calls()
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes')
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name')
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name')
@mock.patch.object(connection.Connection, 'get_volumes')
@mock.patch.object(connection.Connection, 'get_image_name')
@mock.patch.object(connection.Connection, 'get_flavor_name')
def test_get_server_cloud_no_fips(
self,
mock_get_flavor_name, mock_get_image_name,
@ -516,10 +516,10 @@ class TestMeta(base.TestCase):
self.assertEqual(PRIVATE_V4, srv['private_v4'])
self.assert_calls()
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'has_service')
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes')
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name')
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name')
@mock.patch.object(connection.Connection, 'has_service')
@mock.patch.object(connection.Connection, 'get_volumes')
@mock.patch.object(connection.Connection, 'get_image_name')
@mock.patch.object(connection.Connection, 'get_flavor_name')
def test_get_server_cloud_missing_fips(
self,
mock_get_flavor_name, mock_get_image_name,
@ -585,9 +585,9 @@ class TestMeta(base.TestCase):
self.assertEqual(PUBLIC_V4, srv['public_v4'])
self.assert_calls()
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes')
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name')
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name')
@mock.patch.object(connection.Connection, 'get_volumes')
@mock.patch.object(connection.Connection, 'get_image_name')
@mock.patch.object(connection.Connection, 'get_flavor_name')
def test_get_server_cloud_rackspace_v6(
self, mock_get_flavor_name, mock_get_image_name,
mock_get_volumes):
@ -635,9 +635,9 @@ class TestMeta(base.TestCase):
"2001:4800:7819:103:be76:4eff:fe05:8525", srv['interface_ip'])
self.assert_calls()
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes')
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name')
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name')
@mock.patch.object(connection.Connection, 'get_volumes')
@mock.patch.object(connection.Connection, 'get_image_name')
@mock.patch.object(connection.Connection, 'get_flavor_name')
def test_get_server_cloud_osic_split(
self, mock_get_flavor_name, mock_get_image_name,
mock_get_volumes):

View File

@ -15,7 +15,6 @@ import uuid
import testtools
from openstack.cloud import openstackcloud
from openstack.cloud import exc
from openstack import connection
from openstack.tests import fakes
@ -63,7 +62,7 @@ class TestShade(base.TestCase):
# keystoneauth1.loading.base.BaseLoader.load_from_options
self.cloud.connect_as(project_name='test_project')
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'search_images')
@mock.patch.object(connection.Connection, 'search_images')
def test_get_images(self, mock_search):
image1 = dict(id='123', name='mickey')
mock_search.return_value = [image1]
@ -71,7 +70,7 @@ class TestShade(base.TestCase):
self.assertIsNotNone(r)
self.assertDictEqual(image1, r)
@mock.patch.object(openstackcloud._OpenStackCloudMixin, 'search_images')
@mock.patch.object(connection.Connection, 'search_images')
def test_get_image_not_found(self, mock_search):
mock_search.return_value = []
r = self.cloud.get_image('doesNotExist')