| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You 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. |
| """ |
| OpenStack driver |
| """ |
| |
| from libcloud.common.exceptions import BaseHTTPError |
| from libcloud.utils.iso8601 import parse_date |
| |
| try: |
| import simplejson as json |
| except ImportError: |
| import json |
| |
| import warnings |
| import base64 |
| |
| from libcloud.utils.py3 import httplib |
| from libcloud.utils.py3 import b |
| from libcloud.utils.py3 import next |
| from libcloud.utils.py3 import urlparse |
| from libcloud.utils.py3 import parse_qs |
| |
| |
| from libcloud.common.openstack import OpenStackBaseConnection |
| from libcloud.common.openstack import OpenStackDriverMixin |
| from libcloud.common.openstack import OpenStackException |
| from libcloud.common.openstack import OpenStackResponse |
| from libcloud.utils.networking import is_public_subnet |
| from libcloud.compute.base import NodeSize, NodeImage, NodeImageMember, \ |
| UuidMixin |
| from libcloud.compute.base import (NodeDriver, Node, NodeLocation, |
| StorageVolume, VolumeSnapshot) |
| from libcloud.compute.base import KeyPair |
| from libcloud.compute.types import NodeState, StorageVolumeState, Provider, \ |
| VolumeSnapshotState, Type |
| from libcloud.pricing import get_size_price |
| from libcloud.utils.xml import findall |
| from libcloud.utils.py3 import ET |
| |
| __all__ = [ |
| 'OpenStack_1_0_Response', |
| 'OpenStack_1_0_Connection', |
| 'OpenStack_1_0_NodeDriver', |
| 'OpenStack_1_0_SharedIpGroup', |
| 'OpenStack_1_0_NodeIpAddresses', |
| 'OpenStack_1_1_Response', |
| 'OpenStack_1_1_Connection', |
| 'OpenStack_1_1_NodeDriver', |
| 'OpenStack_1_1_FloatingIpPool', |
| 'OpenStack_2_FloatingIpPool', |
| 'OpenStack_1_1_FloatingIpAddress', |
| 'OpenStack_2_PortInterfaceState', |
| 'OpenStack_2_PortInterface', |
| 'OpenStackNodeDriver' |
| ] |
| |
| ATOM_NAMESPACE = "http://www.w3.org/2005/Atom" |
| |
| DEFAULT_API_VERSION = '1.1' |
| |
| PAGINATION_LIMIT = 1000 |
| |
| |
| class OpenStackComputeConnection(OpenStackBaseConnection): |
| # default config for http://devstack.org/ |
| service_type = 'compute' |
| service_name = 'nova' |
| service_region = 'RegionOne' |
| |
| |
| class OpenStackImageConnection(OpenStackBaseConnection): |
| service_type = 'image' |
| service_name = 'glance' |
| service_region = 'RegionOne' |
| |
| |
| class OpenStackNetworkConnection(OpenStackBaseConnection): |
| service_type = 'network' |
| service_name = 'neutron' |
| service_region = 'RegionOne' |
| |
| |
| class OpenStackVolumeV2Connection(OpenStackBaseConnection): |
| service_type = 'volumev2' |
| service_name = 'cinderv2' |
| service_region = 'RegionOne' |
| |
| |
| class OpenStackNodeDriver(NodeDriver, OpenStackDriverMixin): |
| """ |
| Base OpenStack node driver. Should not be used directly. |
| """ |
| api_name = 'openstack' |
| name = 'OpenStack' |
| website = 'http://openstack.org/' |
| |
| NODE_STATE_MAP = { |
| 'BUILD': NodeState.PENDING, |
| 'REBUILD': NodeState.PENDING, |
| 'ACTIVE': NodeState.RUNNING, |
| 'SUSPENDED': NodeState.SUSPENDED, |
| 'SHUTOFF': NodeState.STOPPED, |
| 'DELETED': NodeState.TERMINATED, |
| 'QUEUE_RESIZE': NodeState.PENDING, |
| 'PREP_RESIZE': NodeState.PENDING, |
| 'VERIFY_RESIZE': NodeState.RUNNING, |
| 'PASSWORD': NodeState.PENDING, |
| 'RESCUE': NodeState.PENDING, |
| 'REBOOT': NodeState.REBOOTING, |
| 'HARD_REBOOT': NodeState.REBOOTING, |
| 'SHARE_IP': NodeState.PENDING, |
| 'SHARE_IP_NO_CONFIG': NodeState.PENDING, |
| 'DELETE_IP': NodeState.PENDING, |
| 'ERROR': NodeState.ERROR, |
| 'UNKNOWN': NodeState.UNKNOWN |
| } |
| |
| # http://developer.openstack.org/api-ref-blockstorage-v2.html#volumes-v2 |
| VOLUME_STATE_MAP = { |
| 'creating': StorageVolumeState.CREATING, |
| 'available': StorageVolumeState.AVAILABLE, |
| 'attaching': StorageVolumeState.ATTACHING, |
| 'in-use': StorageVolumeState.INUSE, |
| 'deleting': StorageVolumeState.DELETING, |
| 'error': StorageVolumeState.ERROR, |
| 'error_deleting': StorageVolumeState.ERROR, |
| 'backing-up': StorageVolumeState.BACKUP, |
| 'restoring-backup': StorageVolumeState.BACKUP, |
| 'error_restoring': StorageVolumeState.ERROR, |
| 'error_extending': StorageVolumeState.ERROR, |
| } |
| |
| # http://developer.openstack.org/api-ref-blockstorage-v2.html#ext-backups-v2 |
| SNAPSHOT_STATE_MAP = { |
| 'creating': VolumeSnapshotState.CREATING, |
| 'available': VolumeSnapshotState.AVAILABLE, |
| 'deleting': VolumeSnapshotState.DELETING, |
| 'error': VolumeSnapshotState.ERROR, |
| 'restoring': VolumeSnapshotState.RESTORING, |
| 'error_restoring': VolumeSnapshotState.ERROR |
| } |
| |
| def __new__(cls, key, secret=None, secure=True, host=None, port=None, |
| api_version=DEFAULT_API_VERSION, **kwargs): |
| if cls is OpenStackNodeDriver: |
| if api_version == '1.0': |
| cls = OpenStack_1_0_NodeDriver |
| elif api_version == '1.1': |
| cls = OpenStack_1_1_NodeDriver |
| elif api_version in ['2.0', '2.1', '2.2']: |
| cls = OpenStack_2_NodeDriver |
| else: |
| raise NotImplementedError( |
| "No OpenStackNodeDriver found for API version %s" % |
| (api_version)) |
| return super(OpenStackNodeDriver, cls).__new__(cls) |
| |
| def __init__(self, *args, **kwargs): |
| OpenStackDriverMixin.__init__(self, **kwargs) |
| super(OpenStackNodeDriver, self).__init__(*args, **kwargs) |
| |
| @staticmethod |
| def _paginated_request(url, obj, connection, params=None): |
| """ |
| Perform multiple calls in order to have a full list of elements when |
| the API responses are paginated. |
| |
| :param url: API endpoint |
| :type url: ``str`` |
| |
| :param obj: Result object key |
| :type obj: ``str`` |
| |
| :param connection: The API connection to use to perform the request |
| :type connection: ``obj`` |
| |
| :param params: Any request parameters |
| :type params: ``dict`` |
| |
| :return: ``list`` of API response objects |
| :rtype: ``list`` |
| """ |
| params = params or {} |
| objects = list() |
| loop_count = 0 |
| while True: |
| data = connection.request(url, params=params) |
| values = data.object.get(obj, list()) |
| objects.extend(values) |
| links = data.object.get('%s_links' % obj, list()) |
| next_links = [n for n in links if n['rel'] == 'next'] |
| if next_links: |
| next_link = next_links[0] |
| query = urlparse.urlparse(next_link['href']) |
| # The query[4] references the query parameters from the url |
| params.update(parse_qs(query[4])) |
| else: |
| break |
| |
| # Prevent the pagination from looping indefinitely in case |
| # the API returns a loop for some reason. |
| loop_count += 1 |
| if loop_count > PAGINATION_LIMIT: |
| raise OpenStackException( |
| 'Pagination limit reached for %s, the limit is %d. ' |
| 'This might indicate that your API is returning a ' |
| 'looping next target for pagination!' % ( |
| url, PAGINATION_LIMIT |
| ), None |
| ) |
| return {obj: objects} |
| |
| def _paginated_request_next(self, path, request_method, response_key): |
| """ |
| Perform multiple calls and retrieve all the elements for a paginated |
| response. |
| |
| This method utilizes "next" attribute in the response object. |
| |
| It also includes an infinite loop protection (if the "next" value |
| matches the current path, it will abort). |
| |
| :param request_method: Method to call which will send the request and |
| return a response. This method will get passed |
| in "path" as a first argument. |
| |
| :param response_key: Key in the response object dictionary which |
| contains actual objects we are interested in. |
| """ |
| iteration_count = 0 |
| |
| result = [] |
| while path: |
| response = request_method(path) |
| items = response.object.get(response_key, []) or [] |
| result.extend(items) |
| |
| # Retrieve next path |
| next_path = response.object.get('next', None) |
| |
| if next_path == path: |
| # Likely an infinite loop since the next path matches the |
| # current one |
| break |
| |
| if iteration_count > PAGINATION_LIMIT: |
| # We have iterated over PAGINATION_LIMIT pages, likely an |
| # API returned an invalid response |
| raise OpenStackException( |
| 'Pagination limit reached for %s, the limit is %d. ' |
| 'This might indicate that your API is returning a ' |
| 'looping next target for pagination!' % ( |
| path, PAGINATION_LIMIT |
| ), None |
| ) |
| |
| path = next_path |
| iteration_count += 1 |
| |
| return result |
| |
| def destroy_node(self, node): |
| uri = '/servers/%s' % (node.id) |
| resp = self.connection.request(uri, method='DELETE') |
| # The OpenStack and Rackspace documentation both say this API will |
| # return a 204, but in-fact, everyone everywhere agrees it actually |
| # returns a 202, so we are going to accept either, and someday, |
| # someone will fix either the implementation or the documentation to |
| # agree. |
| return resp.status in (httplib.NO_CONTENT, httplib.ACCEPTED) |
| |
| def reboot_node(self, node): |
| # pylint: disable=no-member |
| return self._reboot_node(node, reboot_type='HARD') |
| |
| def start_node(self, node): |
| # pylint: disable=no-member |
| return self._post_simple_node_action(node, 'os-start') |
| |
| def stop_node(self, node): |
| # pylint: disable=no-member |
| return self._post_simple_node_action(node, 'os-stop') |
| |
| def list_nodes(self, ex_all_tenants=False): |
| """ |
| List the nodes in a tenant |
| |
| :param ex_all_tenants: List nodes for all the tenants. Note: Your user |
| must have admin privileges for this |
| functionality to work. |
| :type ex_all_tenants: ``bool`` |
| """ |
| params = {} |
| if ex_all_tenants: |
| params = {'all_tenants': 1} |
| |
| # pylint: disable=no-member |
| return self._to_nodes( |
| self.connection.request('/servers/detail', params=params).object) |
| |
| def create_volume(self, size, name, location=None, snapshot=None, |
| ex_volume_type=None): |
| """ |
| Create a new volume. |
| |
| :param size: Size of volume in gigabytes (required) |
| :type size: ``int`` |
| |
| :param name: Name of the volume to be created |
| :type name: ``str`` |
| |
| :param location: Which data center to create a volume in. If |
| empty, undefined behavior will be selected. |
| (optional) |
| :type location: :class:`.NodeLocation` |
| |
| :param snapshot: Snapshot from which to create the new |
| volume. (optional) |
| :type snapshot: :class:`.VolumeSnapshot` |
| |
| :param ex_volume_type: What kind of volume to create. |
| (optional) |
| :type ex_volume_type: ``str`` |
| |
| :return: The newly created volume. |
| :rtype: :class:`StorageVolume` |
| """ |
| volume = { |
| 'display_name': name, |
| 'display_description': name, |
| 'size': size, |
| 'metadata': { |
| 'contents': name, |
| }, |
| } |
| |
| if ex_volume_type: |
| volume['volume_type'] = ex_volume_type |
| |
| if location: |
| volume['availability_zone'] = location |
| |
| if snapshot: |
| volume['snapshot_id'] = snapshot.id |
| |
| resp = self.connection.request('/os-volumes', |
| method='POST', |
| data={'volume': volume}) |
| |
| # pylint: disable=no-member |
| return self._to_volume(resp.object) |
| |
| def destroy_volume(self, volume): |
| return self.connection.request('/os-volumes/%s' % volume.id, |
| method='DELETE').success() |
| |
| def attach_volume(self, node, volume, device="auto"): |
| # when "auto" or None is provided for device, openstack will let |
| # the guest OS pick the next available device (fi. /dev/vdb) |
| if device == "auto": |
| device = None |
| return self.connection.request( |
| '/servers/%s/os-volume_attachments' % node.id, |
| method='POST', |
| data={ |
| 'volumeAttachment': { |
| 'volumeId': volume.id, |
| 'device': device, |
| } |
| }).success() |
| |
| def detach_volume(self, volume, ex_node=None): |
| # when ex_node is not provided, volume is detached from all nodes |
| failed_nodes = [] |
| for attachment in volume.extra['attachments']: |
| if not ex_node or ex_node.id in filter(None, (attachment.get( |
| 'serverId' |
| ), attachment.get('server_id'))): |
| response = self.connection.request( |
| '/servers/%s/os-volume_attachments/%s' % |
| (attachment.get('serverId') or attachment['server_id'], |
| attachment['id']), |
| method='DELETE') |
| |
| if not response.success(): |
| failed_nodes.append( |
| attachment.get('serverId') or attachment['server_id'] |
| ) |
| if failed_nodes: |
| raise OpenStackException( |
| 'detach_volume failed for nodes with id: %s' % |
| ', '.join(failed_nodes), 500, self |
| ) |
| return True |
| |
| def list_volumes(self): |
| # pylint: disable=no-member |
| return self._to_volumes( |
| self.connection.request('/os-volumes').object) |
| |
| def ex_get_volume(self, volumeId): |
| # pylint: disable=no-member |
| return self._to_volume( |
| self.connection.request('/os-volumes/%s' % volumeId).object) |
| |
| def list_images(self, location=None, ex_only_active=True): |
| """ |
| Lists all active images |
| |
| @inherits: :class:`NodeDriver.list_images` |
| |
| :param ex_only_active: True if list only active (optional) |
| :type ex_only_active: ``bool`` |
| |
| """ |
| # pylint: disable=no-member |
| return self._to_images( |
| self.connection.request('/images/detail').object, ex_only_active) |
| |
| def get_image(self, image_id): |
| """ |
| Get an image based on an image_id |
| |
| @inherits: :class:`NodeDriver.get_image` |
| |
| :param image_id: Image identifier |
| :type image_id: ``str`` |
| |
| :return: A NodeImage object |
| :rtype: :class:`NodeImage` |
| |
| """ |
| # pylint: disable=no-member |
| return self._to_image(self.connection.request( |
| '/images/%s' % (image_id,)).object['image']) |
| |
| def list_sizes(self, location=None): |
| # pylint: disable=no-member |
| return self._to_sizes( |
| self.connection.request('/flavors/detail').object) |
| |
| def list_locations(self): |
| return [NodeLocation(0, '', '', self)] |
| |
| def _ex_connection_class_kwargs(self): |
| return self.openstack_connection_kwargs() |
| |
| def ex_get_node_details(self, node_id): |
| """ |
| Lists details of the specified server. |
| |
| :param node_id: ID of the node which should be used |
| :type node_id: ``str`` |
| |
| :rtype: :class:`Node` |
| """ |
| # @TODO: Remove this if in 0.6 |
| if isinstance(node_id, Node): |
| node_id = node_id.id |
| |
| uri = '/servers/%s' % (node_id) |
| try: |
| resp = self.connection.request(uri, method='GET') |
| except BaseHTTPError as e: |
| if e.code == httplib.NOT_FOUND: |
| return None |
| raise |
| |
| # pylint: disable=no-member |
| return self._to_node_from_obj(resp.object) |
| |
| def ex_soft_reboot_node(self, node): |
| """ |
| Soft reboots the specified server |
| |
| :param node: node |
| :type node: :class:`Node` |
| |
| :rtype: ``bool`` |
| """ |
| # pylint: disable=no-member |
| return self._reboot_node(node, reboot_type='SOFT') |
| |
| def ex_hard_reboot_node(self, node): |
| """ |
| Hard reboots the specified server |
| |
| :param node: node |
| :type node: :class:`Node` |
| |
| :rtype: ``bool`` |
| """ |
| # pylint: disable=no-member |
| return self._reboot_node(node, reboot_type='HARD') |
| |
| |
| class OpenStackNodeSize(NodeSize): |
| """ |
| NodeSize class for the OpenStack.org driver. |
| |
| Following the example of OpenNebula.org driver |
| and following guidelines: |
| https://issues.apache.org/jira/browse/LIBCLOUD-119 |
| """ |
| |
| def __init__(self, id, name, ram, disk, bandwidth, price, driver, |
| vcpus=None, ephemeral_disk=None, swap=None, extra=None): |
| super(OpenStackNodeSize, self).__init__(id=id, name=name, ram=ram, |
| disk=disk, |
| bandwidth=bandwidth, |
| price=price, driver=driver) |
| self.vcpus = vcpus |
| self.ephemeral_disk = ephemeral_disk |
| self.swap = swap |
| self.extra = extra |
| |
| def __repr__(self): |
| return (('<OpenStackNodeSize: id=%s, name=%s, ram=%s, disk=%s, ' |
| 'bandwidth=%s, price=%s, driver=%s, vcpus=%s, ...>') |
| % (self.id, self.name, self.ram, self.disk, self.bandwidth, |
| self.price, self.driver.name, self.vcpus)) |
| |
| |
| class OpenStack_1_0_Response(OpenStackResponse): |
| def __init__(self, *args, **kwargs): |
| # done because of a circular reference from |
| # NodeDriver -> Connection -> Response |
| self.node_driver = OpenStack_1_0_NodeDriver |
| super(OpenStack_1_0_Response, self).__init__(*args, **kwargs) |
| |
| |
| class OpenStack_1_0_Connection(OpenStackComputeConnection): |
| responseCls = OpenStack_1_0_Response |
| default_content_type = 'application/xml; charset=UTF-8' |
| accept_format = 'application/xml' |
| XML_NAMESPACE = 'http://docs.rackspacecloud.com/servers/api/v1.0' |
| |
| |
| class OpenStack_1_0_NodeDriver(OpenStackNodeDriver): |
| """ |
| OpenStack node driver. |
| |
| Extra node attributes: |
| - password: root password, available after create. |
| - hostId: represents the host your cloud server runs on |
| - imageId: id of image |
| - flavorId: id of flavor |
| """ |
| connectionCls = OpenStack_1_0_Connection |
| type = Provider.OPENSTACK |
| |
| features = {'create_node': ['generates_password']} |
| |
| def __init__(self, *args, **kwargs): |
| self._ex_force_api_version = str(kwargs.pop('ex_force_api_version', |
| None)) |
| self.XML_NAMESPACE = self.connectionCls.XML_NAMESPACE |
| super(OpenStack_1_0_NodeDriver, self).__init__(*args, **kwargs) |
| |
| def _to_images(self, object, ex_only_active): |
| images = [] |
| for image in findall(object, 'image', self.XML_NAMESPACE): |
| if ex_only_active and image.get('status') != 'ACTIVE': |
| continue |
| images.append(self._to_image(image)) |
| |
| return images |
| |
| def _to_image(self, element): |
| return NodeImage(id=element.get('id'), |
| name=element.get('name'), |
| driver=self.connection.driver, |
| extra={'updated': element.get('updated'), |
| 'created': element.get('created'), |
| 'status': element.get('status'), |
| 'serverId': element.get('serverId'), |
| 'progress': element.get('progress'), |
| 'minDisk': element.get('minDisk'), |
| 'minRam': element.get('minRam') |
| } |
| ) |
| |
| def _change_password_or_name(self, node, name=None, password=None): |
| uri = '/servers/%s' % (node.id) |
| |
| if not name: |
| name = node.name |
| |
| body = {'xmlns': self.XML_NAMESPACE, |
| 'name': name} |
| |
| if password is not None: |
| body['adminPass'] = password |
| |
| server_elm = ET.Element('server', body) |
| |
| resp = self.connection.request( |
| uri, method='PUT', data=ET.tostring(server_elm)) |
| |
| if resp.status == httplib.NO_CONTENT and password is not None: |
| node.extra['password'] = password |
| |
| return resp.status == httplib.NO_CONTENT |
| |
| def create_node(self, name, size, image, ex_metadata=None, ex_files=None, |
| ex_shared_ip_group=None, ex_shared_ip_group_id=None): |
| """ |
| Create a new node |
| |
| @inherits: :class:`NodeDriver.create_node` |
| |
| :keyword ex_metadata: Key/Value metadata to associate with a node |
| :type ex_metadata: ``dict`` |
| |
| :keyword ex_files: File Path => File contents to create on |
| the node |
| :type ex_files: ``dict`` |
| |
| :keyword ex_shared_ip_group_id: The server is launched into |
| that shared IP group |
| :type ex_shared_ip_group_id: ``str`` |
| """ |
| attributes = {'xmlns': self.XML_NAMESPACE, |
| 'name': name, |
| 'imageId': str(image.id), |
| 'flavorId': str(size.id)} |
| |
| if ex_shared_ip_group: |
| # Deprecate this. Be explicit and call the variable |
| # ex_shared_ip_group_id since user needs to pass in the id, not the |
| # name. |
| warnings.warn('ex_shared_ip_group argument is deprecated.' |
| ' Please use ex_shared_ip_group_id') |
| |
| if ex_shared_ip_group_id: |
| attributes['sharedIpGroupId'] = ex_shared_ip_group_id |
| |
| server_elm = ET.Element('server', attributes) |
| |
| metadata_elm = self._metadata_to_xml(ex_metadata or {}) |
| if metadata_elm: |
| server_elm.append(metadata_elm) |
| |
| files_elm = self._files_to_xml(ex_files or {}) |
| if files_elm: |
| server_elm.append(files_elm) |
| |
| resp = self.connection.request("/servers", |
| method='POST', |
| data=ET.tostring(server_elm)) |
| return self._to_node(resp.object) |
| |
| def ex_set_password(self, node, password): |
| """ |
| Sets the Node's root password. |
| |
| This will reboot the instance to complete the operation. |
| |
| :class:`Node.extra['password']` will be set to the new value if the |
| operation was successful. |
| |
| :param node: node to set password |
| :type node: :class:`Node` |
| |
| :param password: new password. |
| :type password: ``str`` |
| |
| :rtype: ``bool`` |
| """ |
| return self._change_password_or_name(node, password=password) |
| |
| def ex_set_server_name(self, node, name): |
| """ |
| Sets the Node's name. |
| |
| This will reboot the instance to complete the operation. |
| |
| :param node: node to set name |
| :type node: :class:`Node` |
| |
| :param name: new name |
| :type name: ``str`` |
| |
| :rtype: ``bool`` |
| """ |
| return self._change_password_or_name(node, name=name) |
| |
| def ex_resize_node(self, node, size): |
| """ |
| Change an existing server flavor / scale the server up or down. |
| |
| :param node: node to resize. |
| :type node: :class:`Node` |
| |
| :param size: new size. |
| :type size: :class:`NodeSize` |
| |
| :rtype: ``bool`` |
| """ |
| elm = ET.Element( |
| 'resize', |
| {'xmlns': self.XML_NAMESPACE, |
| 'flavorId': str(size.id)} |
| ) |
| |
| resp = self.connection.request("/servers/%s/action" % (node.id), |
| method='POST', |
| data=ET.tostring(elm)) |
| return resp.status == httplib.ACCEPTED |
| |
| def ex_resize(self, node, size): |
| """ |
| NOTE: This method is here for backward compatibility reasons. |
| |
| You should use ``ex_resize_node`` instead. |
| """ |
| return self.ex_resize_node(node=node, size=size) |
| |
| def ex_confirm_resize(self, node): |
| """ |
| Confirm a resize request which is currently in progress. If a resize |
| request is not explicitly confirmed or reverted it's automatically |
| confirmed after 24 hours. |
| |
| For more info refer to the API documentation: http://goo.gl/zjFI1 |
| |
| :param node: node for which the resize request will be confirmed. |
| :type node: :class:`Node` |
| |
| :rtype: ``bool`` |
| """ |
| elm = ET.Element( |
| 'confirmResize', |
| {'xmlns': self.XML_NAMESPACE}, |
| ) |
| |
| resp = self.connection.request("/servers/%s/action" % (node.id), |
| method='POST', |
| data=ET.tostring(elm)) |
| return resp.status == httplib.NO_CONTENT |
| |
| def ex_revert_resize(self, node): |
| """ |
| Revert a resize request which is currently in progress. |
| All resizes are automatically confirmed after 24 hours if they have |
| not already been confirmed explicitly or reverted. |
| |
| For more info refer to the API documentation: http://goo.gl/AizBu |
| |
| :param node: node for which the resize request will be reverted. |
| :type node: :class:`Node` |
| |
| :rtype: ``bool`` |
| """ |
| elm = ET.Element( |
| 'revertResize', |
| {'xmlns': self.XML_NAMESPACE} |
| ) |
| |
| resp = self.connection.request("/servers/%s/action" % (node.id), |
| method='POST', |
| data=ET.tostring(elm)) |
| return resp.status == httplib.NO_CONTENT |
| |
| def ex_rebuild(self, node_id, image_id): |
| """ |
| Rebuilds the specified server. |
| |
| :param node_id: ID of the node which should be used |
| :type node_id: ``str`` |
| |
| :param image_id: ID of the image which should be used |
| :type image_id: ``str`` |
| |
| :rtype: ``bool`` |
| """ |
| # @TODO: Remove those ifs in 0.6 |
| if isinstance(node_id, Node): |
| node_id = node_id.id |
| |
| if isinstance(image_id, NodeImage): |
| image_id = image_id.id |
| |
| elm = ET.Element( |
| 'rebuild', |
| {'xmlns': self.XML_NAMESPACE, |
| 'imageId': image_id} |
| ) |
| |
| resp = self.connection.request("/servers/%s/action" % node_id, |
| method='POST', |
| data=ET.tostring(elm)) |
| return resp.status == httplib.ACCEPTED |
| |
| def ex_create_ip_group(self, group_name, node_id=None): |
| """ |
| Creates a shared IP group. |
| |
| :param group_name: group name which should be used |
| :type group_name: ``str`` |
| |
| :param node_id: ID of the node which should be used |
| :type node_id: ``str`` |
| |
| :rtype: ``bool`` |
| """ |
| # @TODO: Remove this if in 0.6 |
| if isinstance(node_id, Node): |
| node_id = node_id.id |
| |
| group_elm = ET.Element( |
| 'sharedIpGroup', |
| {'xmlns': self.XML_NAMESPACE, |
| 'name': group_name} |
| ) |
| |
| if node_id: |
| ET.SubElement( |
| group_elm, |
| 'server', |
| {'id': node_id} |
| ) |
| |
| resp = self.connection.request('/shared_ip_groups', |
| method='POST', |
| data=ET.tostring(group_elm)) |
| return self._to_shared_ip_group(resp.object) |
| |
| def ex_list_ip_groups(self, details=False): |
| """ |
| Lists IDs and names for shared IP groups. |
| If details lists all details for shared IP groups. |
| |
| :param details: True if details is required |
| :type details: ``bool`` |
| |
| :rtype: ``list`` of :class:`OpenStack_1_0_SharedIpGroup` |
| """ |
| uri = '/shared_ip_groups/detail' if details else '/shared_ip_groups' |
| resp = self.connection.request(uri, |
| method='GET') |
| groups = findall(resp.object, 'sharedIpGroup', |
| self.XML_NAMESPACE) |
| return [self._to_shared_ip_group(el) for el in groups] |
| |
| def ex_delete_ip_group(self, group_id): |
| """ |
| Deletes the specified shared IP group. |
| |
| :param group_id: group id which should be used |
| :type group_id: ``str`` |
| |
| :rtype: ``bool`` |
| """ |
| uri = '/shared_ip_groups/%s' % group_id |
| resp = self.connection.request(uri, method='DELETE') |
| return resp.status == httplib.NO_CONTENT |
| |
| def ex_share_ip(self, group_id, node_id, ip, configure_node=True): |
| """ |
| Shares an IP address to the specified server. |
| |
| :param group_id: group id which should be used |
| :type group_id: ``str`` |
| |
| :param node_id: ID of the node which should be used |
| :type node_id: ``str`` |
| |
| :param ip: ip which should be used |
| :type ip: ``str`` |
| |
| :param configure_node: configure node |
| :type configure_node: ``bool`` |
| |
| :rtype: ``bool`` |
| """ |
| # @TODO: Remove this if in 0.6 |
| if isinstance(node_id, Node): |
| node_id = node_id.id |
| |
| if configure_node: |
| str_configure = 'true' |
| else: |
| str_configure = 'false' |
| |
| elm = ET.Element( |
| 'shareIp', |
| {'xmlns': self.XML_NAMESPACE, |
| 'sharedIpGroupId': group_id, |
| 'configureServer': str_configure}, |
| ) |
| |
| uri = '/servers/%s/ips/public/%s' % (node_id, ip) |
| |
| resp = self.connection.request(uri, |
| method='PUT', |
| data=ET.tostring(elm)) |
| return resp.status == httplib.ACCEPTED |
| |
| def ex_unshare_ip(self, node_id, ip): |
| """ |
| Removes a shared IP address from the specified server. |
| |
| :param node_id: ID of the node which should be used |
| :type node_id: ``str`` |
| |
| :param ip: ip which should be used |
| :type ip: ``str`` |
| |
| :rtype: ``bool`` |
| """ |
| # @TODO: Remove this if in 0.6 |
| if isinstance(node_id, Node): |
| node_id = node_id.id |
| |
| uri = '/servers/%s/ips/public/%s' % (node_id, ip) |
| |
| resp = self.connection.request(uri, |
| method='DELETE') |
| return resp.status == httplib.ACCEPTED |
| |
| def ex_list_ip_addresses(self, node_id): |
| """ |
| List all server addresses. |
| |
| :param node_id: ID of the node which should be used |
| :type node_id: ``str`` |
| |
| :rtype: :class:`OpenStack_1_0_NodeIpAddresses` |
| """ |
| # @TODO: Remove this if in 0.6 |
| if isinstance(node_id, Node): |
| node_id = node_id.id |
| |
| uri = '/servers/%s/ips' % node_id |
| resp = self.connection.request(uri, |
| method='GET') |
| return self._to_ip_addresses(resp.object) |
| |
| def _metadata_to_xml(self, metadata): |
| if not metadata: |
| return None |
| |
| metadata_elm = ET.Element('metadata') |
| for k, v in list(metadata.items()): |
| meta_elm = ET.SubElement(metadata_elm, 'meta', {'key': str(k)}) |
| meta_elm.text = str(v) |
| |
| return metadata_elm |
| |
| def _files_to_xml(self, files): |
| if not files: |
| return None |
| |
| personality_elm = ET.Element('personality') |
| for k, v in list(files.items()): |
| file_elm = ET.SubElement(personality_elm, |
| 'file', |
| {'path': str(k)}) |
| file_elm.text = base64.b64encode(b(v)).decode('ascii') |
| |
| return personality_elm |
| |
| def _reboot_node(self, node, reboot_type='SOFT'): |
| resp = self._node_action(node, ['reboot', ('type', reboot_type)]) |
| return resp.status == httplib.ACCEPTED |
| |
| def _node_action(self, node, body): |
| if isinstance(body, list): |
| attr = ' '.join(['%s="%s"' % (item[0], item[1]) |
| for item in body[1:]]) |
| body = '<%s xmlns="%s" %s/>' % (body[0], self.XML_NAMESPACE, attr) |
| uri = '/servers/%s/action' % (node.id) |
| resp = self.connection.request(uri, method='POST', data=body) |
| return resp |
| |
| def _to_nodes(self, object): |
| node_elements = findall(object, 'server', self.XML_NAMESPACE) |
| return [self._to_node(el) for el in node_elements] |
| |
| def _to_node_from_obj(self, obj): |
| return self._to_node(findall(obj, 'server', self.XML_NAMESPACE)[0]) |
| |
| def _to_node(self, el): |
| def get_ips(el): |
| return [ip.get('addr') for ip in el] |
| |
| def get_meta_dict(el): |
| d = {} |
| for meta in el: |
| d[meta.get('key')] = meta.text |
| return d |
| |
| public_ip = get_ips(findall(el, 'addresses/public/ip', |
| self.XML_NAMESPACE)) |
| private_ip = get_ips(findall(el, 'addresses/private/ip', |
| self.XML_NAMESPACE)) |
| metadata = get_meta_dict(findall(el, 'metadata/meta', |
| self.XML_NAMESPACE)) |
| |
| n = Node(id=el.get('id'), |
| name=el.get('name'), |
| state=self.NODE_STATE_MAP.get( |
| el.get('status'), NodeState.UNKNOWN), |
| public_ips=public_ip, |
| private_ips=private_ip, |
| driver=self.connection.driver, |
| # pylint: disable=no-member |
| extra={ |
| 'password': el.get('adminPass'), |
| 'hostId': el.get('hostId'), |
| 'imageId': el.get('imageId'), |
| 'flavorId': el.get('flavorId'), |
| 'uri': "https://%s%s/servers/%s" % ( |
| self.connection.host, |
| self.connection.request_path, el.get('id')), |
| 'service_name': self.connection.get_service_name(), |
| 'metadata': metadata}) |
| return n |
| |
| def _to_sizes(self, object): |
| elements = findall(object, 'flavor', self.XML_NAMESPACE) |
| return [self._to_size(el) for el in elements] |
| |
| def _to_size(self, el): |
| vcpus = int(el.get('vcpus')) if el.get('vcpus', None) else None |
| return OpenStackNodeSize(id=el.get('id'), |
| name=el.get('name'), |
| ram=int(el.get('ram')), |
| disk=int(el.get('disk')), |
| # XXX: needs hardcode |
| vcpus=vcpus, |
| bandwidth=None, |
| extra=el.get('extra_specs'), |
| # Hardcoded |
| price=self._get_size_price(el.get('id')), |
| driver=self.connection.driver) |
| |
| def ex_limits(self): |
| """ |
| Extra call to get account's limits, such as |
| rates (for example amount of POST requests per day) |
| and absolute limits like total amount of available |
| RAM to be used by servers. |
| |
| :return: dict with keys 'rate' and 'absolute' |
| :rtype: ``dict`` |
| """ |
| |
| def _to_rate(el): |
| rate = {} |
| for item in list(el.items()): |
| rate[item[0]] = item[1] |
| |
| return rate |
| |
| def _to_absolute(el): |
| return {el.get('name'): el.get('value')} |
| |
| limits = self.connection.request("/limits").object |
| rate = [_to_rate(el) for el in findall(limits, 'rate/limit', |
| self.XML_NAMESPACE)] |
| absolute = {} |
| for item in findall(limits, 'absolute/limit', |
| self.XML_NAMESPACE): |
| absolute.update(_to_absolute(item)) |
| |
| return {"rate": rate, "absolute": absolute} |
| |
| def create_image(self, node, name, description=None, reboot=True): |
| """Create an image for node. |
| |
| @inherits: :class:`NodeDriver.create_image` |
| |
| :param node: node to use as a base for image |
| :type node: :class:`Node` |
| |
| :param name: name for new image |
| :type name: ``str`` |
| |
| :rtype: :class:`NodeImage` |
| """ |
| |
| image_elm = ET.Element( |
| 'image', |
| {'xmlns': self.XML_NAMESPACE, |
| 'name': name, |
| 'serverId': node.id} |
| ) |
| |
| return self._to_image( |
| self.connection.request("/images", method="POST", |
| data=ET.tostring(image_elm)).object) |
| |
| def delete_image(self, image): |
| """Delete an image for node. |
| |
| @inherits: :class:`NodeDriver.delete_image` |
| |
| :param image: the image to be deleted |
| :type image: :class:`NodeImage` |
| |
| :rtype: ``bool`` |
| """ |
| uri = '/images/%s' % image.id |
| resp = self.connection.request(uri, method='DELETE') |
| return resp.status == httplib.NO_CONTENT |
| |
| def _to_shared_ip_group(self, el): |
| servers_el = findall(el, 'servers', self.XML_NAMESPACE) |
| if servers_el: |
| servers = [s.get('id') |
| for s in findall(servers_el[0], 'server', |
| self.XML_NAMESPACE)] |
| else: |
| servers = None |
| return OpenStack_1_0_SharedIpGroup(id=el.get('id'), |
| name=el.get('name'), |
| servers=servers) |
| |
| def _to_ip_addresses(self, el): |
| public_ips = [ip.get('addr') for ip in findall( |
| findall(el, 'public', self.XML_NAMESPACE)[0], |
| 'ip', self.XML_NAMESPACE)] |
| private_ips = [ip.get('addr') for ip in findall( |
| findall(el, 'private', self.XML_NAMESPACE)[0], |
| 'ip', self.XML_NAMESPACE)] |
| |
| return OpenStack_1_0_NodeIpAddresses(public_ips, private_ips) |
| |
| def _get_size_price(self, size_id): |
| try: |
| return get_size_price(driver_type='compute', |
| driver_name=self.api_name, |
| size_id=size_id) |
| except KeyError: |
| return 0.0 |
| |
| |
| class OpenStack_1_0_SharedIpGroup(object): |
| """ |
| Shared IP group info. |
| """ |
| |
| def __init__(self, id, name, servers=None): |
| self.id = str(id) |
| self.name = name |
| self.servers = servers |
| |
| |
| class OpenStack_1_0_NodeIpAddresses(object): |
| """ |
| List of public and private IP addresses of a Node. |
| """ |
| |
| def __init__(self, public_addresses, private_addresses): |
| self.public_addresses = public_addresses |
| self.private_addresses = private_addresses |
| |
| |
| class OpenStack_1_1_Response(OpenStackResponse): |
| def __init__(self, *args, **kwargs): |
| # done because of a circular reference from |
| # NodeDriver -> Connection -> Response |
| self.node_driver = OpenStack_1_1_NodeDriver |
| super(OpenStack_1_1_Response, self).__init__(*args, **kwargs) |
| |
| |
| class OpenStackNetwork(object): |
| """ |
| A Virtual Network. |
| """ |
| |
| def __init__(self, id, name, cidr, driver, extra=None): |
| self.id = str(id) |
| self.name = name |
| self.cidr = cidr |
| self.driver = driver |
| self.extra = extra or {} |
| |
| def __repr__(self): |
| return '<OpenStackNetwork id="%s" name="%s" cidr="%s">' % (self.id, |
| self.name, |
| self.cidr,) |
| |
| |
| class OpenStackSecurityGroup(object): |
| """ |
| A Security Group. |
| """ |
| |
| def __init__(self, id, tenant_id, name, description, driver, rules=None, |
| extra=None): |
| """ |
| Constructor. |
| |
| :keyword id: Group id. |
| :type id: ``str`` |
| |
| :keyword tenant_id: Owner of the security group. |
| :type tenant_id: ``str`` |
| |
| :keyword name: Human-readable name for the security group. Might |
| not be unique. |
| :type name: ``str`` |
| |
| :keyword description: Human-readable description of a security |
| group. |
| :type description: ``str`` |
| |
| :keyword rules: Rules associated with this group. |
| :type rules: ``list`` of |
| :class:`OpenStackSecurityGroupRule` |
| |
| :keyword extra: Extra attributes associated with this group. |
| :type extra: ``dict`` |
| """ |
| self.id = id |
| self.tenant_id = tenant_id |
| self.name = name |
| self.description = description |
| self.driver = driver |
| self.rules = rules or [] |
| self.extra = extra or {} |
| |
| def __repr__(self): |
| return ('<OpenStackSecurityGroup id=%s tenant_id=%s name=%s \ |
| description=%s>' % (self.id, self.tenant_id, self.name, |
| self.description)) |
| |
| |
| class OpenStackSecurityGroupRule(object): |
| """ |
| A Rule of a Security Group. |
| """ |
| |
| def __init__(self, id, parent_group_id, ip_protocol, from_port, to_port, |
| driver, ip_range=None, group=None, tenant_id=None, |
| direction=None, extra=None): |
| """ |
| Constructor. |
| |
| :keyword id: Rule id. |
| :type id: ``str`` |
| |
| :keyword parent_group_id: ID of the parent security group. |
| :type parent_group_id: ``str`` |
| |
| :keyword ip_protocol: IP Protocol (icmp, tcp, udp, etc). |
| :type ip_protocol: ``str`` |
| |
| :keyword from_port: Port at start of range. |
| :type from_port: ``int`` |
| |
| :keyword to_port: Port at end of range. |
| :type to_port: ``int`` |
| |
| :keyword ip_range: CIDR for address range. |
| :type ip_range: ``str`` |
| |
| :keyword group: Name of a source security group to apply to rule. |
| :type group: ``str`` |
| |
| :keyword tenant_id: Owner of the security group. |
| :type tenant_id: ``str`` |
| |
| :keyword direction: Security group Direction (ingress or egress). |
| :type direction: ``str`` |
| |
| :keyword extra: Extra attributes associated with this rule. |
| :type extra: ``dict`` |
| """ |
| self.id = id |
| self.parent_group_id = parent_group_id |
| self.ip_protocol = ip_protocol |
| self.from_port = from_port |
| self.to_port = to_port |
| self.driver = driver |
| self.ip_range = '' |
| self.group = {} |
| self.direction = 'ingress' |
| |
| if group is None: |
| self.ip_range = ip_range |
| else: |
| self.group = {'name': group, 'tenant_id': tenant_id} |
| |
| # by default in old versions only ingress was used |
| if direction is not None: |
| if direction in ['ingress', 'egress']: |
| self.direction = direction |
| else: |
| raise OpenStackException("Security group direction incorrect " |
| "value: ingress or egress.", 500, |
| driver) |
| |
| self.tenant_id = tenant_id |
| self.extra = extra or {} |
| |
| def __repr__(self): |
| return ('<OpenStackSecurityGroupRule id=%s parent_group_id=%s \ |
| ip_protocol=%s from_port=%s to_port=%s>' % (self.id, |
| self.parent_group_id, self.ip_protocol, self.from_port, |
| self.to_port)) |
| |
| |
| class OpenStackKeyPair(object): |
| """ |
| A KeyPair. |
| """ |
| |
| def __init__(self, name, fingerprint, public_key, driver, private_key=None, |
| extra=None): |
| """ |
| Constructor. |
| |
| :keyword name: Name of the KeyPair. |
| :type name: ``str`` |
| |
| :keyword fingerprint: Fingerprint of the KeyPair |
| :type fingerprint: ``str`` |
| |
| :keyword public_key: Public key in OpenSSH format. |
| :type public_key: ``str`` |
| |
| :keyword private_key: Private key in PEM format. |
| :type private_key: ``str`` |
| |
| :keyword extra: Extra attributes associated with this KeyPair. |
| :type extra: ``dict`` |
| """ |
| self.name = name |
| self.fingerprint = fingerprint |
| self.public_key = public_key |
| self.private_key = private_key |
| self.driver = driver |
| self.extra = extra or {} |
| |
| def __repr__(self): |
| return ('<OpenStackKeyPair name=%s fingerprint=%s public_key=%s ...>' |
| % (self.name, self.fingerprint, self.public_key)) |
| |
| |
| class OpenStack_1_1_Connection(OpenStackComputeConnection): |
| responseCls = OpenStack_1_1_Response |
| accept_format = 'application/json' |
| default_content_type = 'application/json; charset=UTF-8' |
| |
| def encode_data(self, data): |
| return json.dumps(data) |
| |
| |
| class OpenStack_1_1_NodeDriver(OpenStackNodeDriver): |
| """ |
| OpenStack node driver. |
| """ |
| connectionCls = OpenStack_1_1_Connection |
| type = Provider.OPENSTACK |
| |
| features = {"create_node": ["generates_password"]} |
| _networks_url_prefix = '/os-networks' |
| |
| def __init__(self, *args, **kwargs): |
| self._ex_force_api_version = str(kwargs.pop('ex_force_api_version', |
| None)) |
| super(OpenStack_1_1_NodeDriver, self).__init__(*args, **kwargs) |
| |
| def create_node(self, name, size, image=None, ex_keyname=None, |
| ex_userdata=None, |
| ex_config_drive=None, ex_security_groups=None, |
| ex_metadata=None, ex_files=None, networks=None, |
| ex_disk_config=None, |
| ex_admin_pass=None, |
| ex_availability_zone=None, ex_blockdevicemappings=None): |
| """Create a new node |
| |
| @inherits: :class:`NodeDriver.create_node` |
| |
| :keyword ex_keyname: The name of the key pair |
| :type ex_keyname: ``str`` |
| |
| :keyword ex_userdata: String containing user data |
| see |
| https://help.ubuntu.com/community/CloudInit |
| :type ex_userdata: ``str`` |
| |
| :keyword ex_config_drive: Enable config drive |
| see |
| http://docs.openstack.org/grizzly/openstack-compute/admin/content/config-drive.html |
| :type ex_config_drive: ``bool`` |
| |
| :keyword ex_security_groups: List of security groups to assign to |
| the node |
| :type ex_security_groups: ``list`` of |
| :class:`OpenStackSecurityGroup` |
| |
| :keyword ex_metadata: Key/Value metadata to associate with a node |
| :type ex_metadata: ``dict`` |
| |
| :keyword ex_files: File Path => File contents to create on |
| the node |
| :type ex_files: ``dict`` |
| |
| |
| :keyword networks: The server is launched into a set of Networks. |
| :type networks: ``list`` of :class:`OpenStackNetwork` |
| |
| :keyword ex_disk_config: Name of the disk configuration. |
| Can be either ``AUTO`` or ``MANUAL``. |
| :type ex_disk_config: ``str`` |
| |
| :keyword ex_config_drive: If True enables metadata injection in a |
| server through a configuration drive. |
| :type ex_config_drive: ``bool`` |
| |
| :keyword ex_admin_pass: The root password for the node |
| :type ex_admin_pass: ``str`` |
| |
| :keyword ex_availability_zone: Nova availability zone for the node |
| :type ex_availability_zone: ``str`` |
| """ |
| ex_metadata = ex_metadata or {} |
| ex_files = ex_files or {} |
| networks = networks or [] |
| ex_security_groups = ex_security_groups or [] |
| |
| server_params = self._create_args_to_params( |
| node=None, |
| name=name, |
| size=size, image=image, ex_keyname=ex_keyname, |
| ex_userdata=ex_userdata, ex_config_drive=ex_config_drive, |
| ex_security_groups=ex_security_groups, ex_metadata=ex_metadata, |
| ex_files=ex_files, networks=networks, |
| ex_disk_config=ex_disk_config, |
| ex_availability_zone=ex_availability_zone, |
| ex_blockdevicemappings=ex_blockdevicemappings) |
| |
| resp = self.connection.request("/servers", |
| method='POST', |
| data={'server': server_params}) |
| |
| create_response = resp.object['server'] |
| server_resp = self.connection.request( |
| '/servers/%s' % create_response['id']) |
| server_object = server_resp.object['server'] |
| |
| # adminPass is not always present |
| # http://docs.openstack.org/essex/openstack-compute/admin/ |
| # content/configuring-compute-API.html#d6e1833 |
| server_object['adminPass'] = create_response.get('adminPass', None) |
| |
| return self._to_node(server_object) |
| |
| def _to_images(self, obj, ex_only_active): |
| images = [] |
| for image in obj['images']: |
| if ex_only_active and image.get('status') != 'ACTIVE': |
| continue |
| images.append(self._to_image(image)) |
| |
| return images |
| |
| def _to_image(self, api_image): |
| server = api_image.get('server', {}) |
| updated = api_image.get('updated_at') or api_image['updated'] |
| created = api_image.get('created_at') or api_image['created'] |
| min_ram = api_image.get('min_ram') |
| |
| if min_ram is None: |
| min_ram = api_image.get('minRam') |
| |
| min_disk = api_image.get('min_disk') |
| |
| if min_disk is None: |
| min_disk = api_image.get('minDisk') |
| |
| return NodeImage( |
| id=api_image['id'], |
| name=api_image['name'], |
| driver=self, |
| extra=dict( |
| visibility=api_image.get('visibility'), |
| updated=updated, |
| created=created, |
| status=api_image['status'], |
| progress=api_image.get('progress'), |
| metadata=api_image.get('metadata'), |
| os_type=api_image.get('os_type'), |
| serverId=server.get('id'), |
| minDisk=min_disk, |
| minRam=min_ram, |
| ) |
| ) |
| |
| def _to_image_member(self, api_image_member): |
| created = api_image_member['created_at'] |
| updated = api_image_member.get('updated_at') |
| return NodeImageMember( |
| id=api_image_member['member_id'], |
| image_id=api_image_member['image_id'], |
| state=api_image_member['status'], |
| created=created, |
| driver=self, |
| extra=dict( |
| schema=api_image_member.get('schema'), |
| updated=updated, |
| ) |
| ) |
| |
| def _to_nodes(self, obj): |
| servers = obj['servers'] |
| return [self._to_node(server) for server in servers] |
| |
| def _to_volumes(self, obj): |
| volumes = obj['volumes'] |
| return [self._to_volume(volume) for volume in volumes] |
| |
| def _to_snapshots(self, obj): |
| snapshots = obj['snapshots'] |
| return [self._to_snapshot(snapshot) for snapshot in snapshots] |
| |
| def _to_sizes(self, obj): |
| flavors = obj['flavors'] |
| return [self._to_size(flavor) for flavor in flavors] |
| |
| def _create_args_to_params(self, node, **kwargs): |
| server_params = { |
| 'name': kwargs.get('name'), |
| 'metadata': kwargs.get('ex_metadata', {}) or {}, |
| 'personality': self._files_to_personality(kwargs.get("ex_files", |
| {}) or {}) |
| } |
| |
| if kwargs.get('ex_availability_zone', None): |
| server_params['availability_zone'] = kwargs['ex_availability_zone'] |
| |
| if kwargs.get('ex_keyname', None): |
| server_params['key_name'] = kwargs['ex_keyname'] |
| |
| if kwargs.get('ex_userdata', None): |
| server_params['user_data'] = base64.b64encode( |
| b(kwargs['ex_userdata'])).decode('ascii') |
| |
| if kwargs.get('ex_disk_config', None): |
| server_params['OS-DCF:diskConfig'] = kwargs['ex_disk_config'] |
| |
| if kwargs.get('ex_config_drive', None): |
| server_params['config_drive'] = str(kwargs['ex_config_drive']) |
| |
| if kwargs.get('ex_admin_pass', None): |
| server_params['adminPass'] = kwargs['ex_admin_pass'] |
| |
| if kwargs.get('networks', None): |
| networks = kwargs['networks'] or [] |
| networks = [{'uuid': network.id} for network in networks] |
| server_params['networks'] = networks |
| |
| if kwargs.get('ex_security_groups', None): |
| server_params['security_groups'] = [] |
| for security_group in kwargs['ex_security_groups'] or []: |
| name = security_group.name |
| server_params['security_groups'].append({'name': name}) |
| |
| if kwargs.get('ex_blockdevicemappings', None): |
| server_params['block_device_mapping_v2'] = \ |
| kwargs['ex_blockdevicemappings'] |
| |
| if kwargs.get('name', None): |
| server_params['name'] = kwargs.get('name') |
| else: |
| server_params['name'] = node.name |
| |
| if kwargs.get('image', None): |
| server_params['imageRef'] = kwargs.get('image').id |
| else: |
| server_params['imageRef'] = node.extra.get( |
| 'imageId', '' |
| ) if node else '' |
| |
| if kwargs.get('size', None): |
| server_params['flavorRef'] = kwargs.get('size').id |
| else: |
| server_params['flavorRef'] = node.extra.get('flavorId') |
| |
| return server_params |
| |
| def _files_to_personality(self, files): |
| rv = [] |
| |
| for k, v in list(files.items()): |
| rv.append({'path': k, 'contents': base64.b64encode(b(v))}) |
| |
| return rv |
| |
| def _reboot_node(self, node, reboot_type='SOFT'): |
| resp = self._node_action(node, 'reboot', type=reboot_type) |
| return resp.status == httplib.ACCEPTED |
| |
| def ex_set_password(self, node, password): |
| """ |
| Changes the administrator password for a specified server. |
| |
| :param node: Node to rebuild. |
| :type node: :class:`Node` |
| |
| :param password: The administrator password. |
| :type password: ``str`` |
| |
| :rtype: ``bool`` |
| """ |
| resp = self._node_action(node, 'changePassword', adminPass=password) |
| node.extra['password'] = password |
| return resp.status == httplib.ACCEPTED |
| |
| def ex_rebuild(self, node, image, **kwargs): |
| """ |
| Rebuild a Node. |
| |
| :param node: Node to rebuild. |
| :type node: :class:`Node` |
| |
| :param image: New image to use. |
| :type image: :class:`NodeImage` |
| |
| :keyword ex_metadata: Key/Value metadata to associate with a node |
| :type ex_metadata: ``dict`` |
| |
| :keyword ex_files: File Path => File contents to create on |
| the node |
| :type ex_files: ``dict`` |
| |
| :keyword ex_keyname: Name of existing public key to inject into |
| instance |
| :type ex_keyname: ``str`` |
| |
| :keyword ex_userdata: String containing user data |
| see |
| https://help.ubuntu.com/community/CloudInit |
| :type ex_userdata: ``str`` |
| |
| :keyword ex_security_groups: List of security groups to assign to |
| the node |
| :type ex_security_groups: ``list`` of |
| :class:`OpenStackSecurityGroup` |
| |
| :keyword ex_disk_config: Name of the disk configuration. |
| Can be either ``AUTO`` or ``MANUAL``. |
| :type ex_disk_config: ``str`` |
| |
| :keyword ex_config_drive: If True enables metadata injection in a |
| server through a configuration drive. |
| :type ex_config_drive: ``bool`` |
| |
| :rtype: ``bool`` |
| """ |
| server_params = self._create_args_to_params(node, image=image, |
| **kwargs) |
| resp = self._node_action(node, 'rebuild', **server_params) |
| return resp.status == httplib.ACCEPTED |
| |
| def ex_resize(self, node, size): |
| """ |
| Change a node size. |
| |
| :param node: Node to resize. |
| :type node: :class:`Node` |
| |
| :type size: :class:`NodeSize` |
| :param size: New size to use. |
| |
| :rtype: ``bool`` |
| """ |
| server_params = {'flavorRef': size.id} |
| resp = self._node_action(node, 'resize', **server_params) |
| return resp.status == httplib.ACCEPTED |
| |
| def ex_confirm_resize(self, node): |
| """ |
| Confirms a pending resize action. |
| |
| :param node: Node to resize. |
| :type node: :class:`Node` |
| |
| :rtype: ``bool`` |
| """ |
| resp = self._node_action(node, 'confirmResize') |
| return resp.status == httplib.NO_CONTENT |
| |
| def ex_revert_resize(self, node): |
| """ |
| Cancels and reverts a pending resize action. |
| |
| :param node: Node to resize. |
| :type node: :class:`Node` |
| |
| :rtype: ``bool`` |
| """ |
| resp = self._node_action(node, 'revertResize') |
| return resp.status == httplib.ACCEPTED |
| |
| def create_image(self, node, name, metadata=None): |
| """ |
| Creates a new image. |
| |
| :param node: Node |
| :type node: :class:`Node` |
| |
| :param name: The name for the new image. |
| :type name: ``str`` |
| |
| :param metadata: Key and value pairs for metadata. |
| :type metadata: ``dict`` |
| |
| :rtype: :class:`NodeImage` |
| """ |
| optional_params = {} |
| if metadata: |
| optional_params['metadata'] = metadata |
| resp = self._node_action(node, 'createImage', name=name, |
| **optional_params) |
| image_id = self._extract_image_id_from_url(resp.headers['location']) |
| return self.get_image(image_id=image_id) |
| |
| def ex_set_server_name(self, node, name): |
| """ |
| Sets the Node's name. |
| |
| :param node: Node |
| :type node: :class:`Node` |
| |
| :param name: The name of the server. |
| :type name: ``str`` |
| |
| :rtype: :class:`Node` |
| """ |
| return self._update_node(node, name=name) |
| |
| def ex_get_metadata(self, node): |
| """ |
| Get a Node's metadata. |
| |
| :param node: Node |
| :type node: :class:`Node` |
| |
| :return: Key/Value metadata associated with node. |
| :rtype: ``dict`` |
| """ |
| return self.connection.request( |
| '/servers/%s/metadata' % (node.id,), |
| method='GET',).object['metadata'] |
| |
| def ex_set_metadata(self, node, metadata): |
| """ |
| Sets the Node's metadata. |
| |
| :param node: Node |
| :type node: :class:`Node` |
| |
| :param metadata: Key/Value metadata to associate with a node |
| :type metadata: ``dict`` |
| |
| :rtype: ``dict`` |
| """ |
| return self.connection.request( |
| '/servers/%s/metadata' % (node.id,), method='PUT', |
| data={'metadata': metadata} |
| ).object['metadata'] |
| |
| def ex_update_node(self, node, **node_updates): |
| """ |
| Update the Node's editable attributes. The OpenStack API currently |
| supports editing name and IPv4/IPv6 access addresses. |
| |
| The driver currently only supports updating the node name. |
| |
| :param node: Node |
| :type node: :class:`Node` |
| |
| :keyword name: New name for the server |
| :type name: ``str`` |
| |
| :rtype: :class:`Node` |
| """ |
| potential_data = self._create_args_to_params(node, **node_updates) |
| updates = {'name': potential_data['name']} |
| return self._update_node(node, **updates) |
| |
| def _to_networks(self, obj): |
| networks = obj['networks'] |
| return [self._to_network(network) for network in networks] |
| |
| def _to_network(self, obj): |
| return OpenStackNetwork(id=obj['id'], |
| name=obj['label'], |
| cidr=obj.get('cidr', None), |
| driver=self) |
| |
| def ex_list_networks(self): |
| """ |
| Get a list of Networks that are available. |
| |
| :rtype: ``list`` of :class:`OpenStackNetwork` |
| """ |
| response = self.connection.request(self._networks_url_prefix).object |
| return self._to_networks(response) |
| |
| def ex_get_network(self, network_id): |
| """ |
| Retrieve the Network with the given ID |
| |
| :param networkId: ID of the network |
| :type networkId: ``str`` |
| |
| :rtype :class:`OpenStackNetwork` |
| """ |
| request_url = "{networks_url_prefix}/{network_id}".format( |
| networks_url_prefix=self._networks_url_prefix, |
| network_id=network_id |
| ) |
| response = self.connection.request(request_url).object |
| return self._to_network(response['network']) |
| |
| def ex_create_network(self, name, cidr): |
| """ |
| Create a new Network |
| |
| :param name: Name of network which should be used |
| :type name: ``str`` |
| |
| :param cidr: cidr of network which should be used |
| :type cidr: ``str`` |
| |
| :rtype: :class:`OpenStackNetwork` |
| """ |
| data = {'network': {'cidr': cidr, 'label': name}} |
| response = self.connection.request(self._networks_url_prefix, |
| method='POST', data=data).object |
| return self._to_network(response['network']) |
| |
| def ex_delete_network(self, network): |
| """ |
| Delete a Network |
| |
| :param network: Network which should be used |
| :type network: :class:`OpenStackNetwork` |
| |
| :rtype: ``bool`` |
| """ |
| resp = self.connection.request('%s/%s' % (self._networks_url_prefix, |
| network.id), |
| method='DELETE') |
| return resp.status in (httplib.NO_CONTENT, httplib.ACCEPTED) |
| |
| def ex_get_console_output(self, node, length=None): |
| """ |
| Get console output |
| |
| :param node: node |
| :type node: :class:`Node` |
| |
| :param length: Optional number of lines to fetch from the |
| console log |
| :type length: ``int`` |
| |
| :return: Dictionary with the output |
| :rtype: ``dict`` |
| """ |
| |
| data = { |
| "os-getConsoleOutput": { |
| "length": length |
| } |
| } |
| |
| resp = self.connection.request('/servers/%s/action' % node.id, |
| method='POST', data=data).object |
| return resp |
| |
| def ex_list_snapshots(self): |
| return self._to_snapshots( |
| self.connection.request('/os-snapshots').object) |
| |
| def ex_get_snapshot(self, snapshotId): |
| return self._to_snapshot( |
| self.connection.request('/os-snapshots/%s' % snapshotId).object) |
| |
| def list_volume_snapshots(self, volume): |
| return [snapshot for snapshot in self.ex_list_snapshots() |
| if snapshot.extra['volume_id'] == volume.id] |
| |
| def create_volume_snapshot(self, volume, name=None, ex_description=None, |
| ex_force=True): |
| """ |
| Create snapshot from volume |
| |
| :param volume: Instance of `StorageVolume` |
| :type volume: `StorageVolume` |
| |
| :param name: Name of snapshot (optional) |
| :type name: `str` | `NoneType` |
| |
| :param ex_description: Description of the snapshot (optional) |
| :type ex_description: `str` | `NoneType` |
| |
| :param ex_force: Specifies if we create a snapshot that is not in |
| state `available`. For example `in-use`. Defaults |
| to True. (optional) |
| :type ex_force: `bool` |
| |
| :rtype: :class:`VolumeSnapshot` |
| """ |
| data = {'snapshot': {'volume_id': volume.id, 'force': ex_force}} |
| |
| if name is not None: |
| data['snapshot']['display_name'] = name |
| |
| if ex_description is not None: |
| data['snapshot']['display_description'] = ex_description |
| |
| return self._to_snapshot(self.connection.request('/os-snapshots', |
| method='POST', |
| data=data).object) |
| |
| def destroy_volume_snapshot(self, snapshot): |
| resp = self.connection.request('/os-snapshots/%s' % snapshot.id, |
| method='DELETE') |
| return resp.status == httplib.NO_CONTENT |
| |
| def ex_create_snapshot(self, volume, name, description=None, force=False): |
| """ |
| Create a snapshot based off of a volume. |
| |
| :param volume: volume |
| :type volume: :class:`StorageVolume` |
| |
| :keyword name: New name for the volume snapshot |
| :type name: ``str`` |
| |
| :keyword description: Description of the snapshot (optional) |
| :type description: ``str`` |
| |
| :keyword force: Whether to force creation (optional) |
| :type force: ``bool`` |
| |
| :rtype: :class:`VolumeSnapshot` |
| """ |
| warnings.warn('This method has been deprecated in favor of the ' |
| 'create_volume_snapshot method') |
| return self.create_volume_snapshot(volume, name, |
| ex_description=description, |
| ex_force=force) |
| |
| def ex_delete_snapshot(self, snapshot): |
| """ |
| Delete a VolumeSnapshot |
| |
| :param snapshot: snapshot |
| :type snapshot: :class:`VolumeSnapshot` |
| |
| :rtype: ``bool`` |
| """ |
| warnings.warn('This method has been deprecated in favor of the ' |
| 'destroy_volume_snapshot method') |
| return self.destroy_volume_snapshot(snapshot) |
| |
| def _to_security_group_rules(self, obj): |
| return [self._to_security_group_rule(security_group_rule) for |
| security_group_rule in obj] |
| |
| def _to_security_group_rule(self, obj): |
| ip_range = group = tenant_id = None |
| if obj['group'] == {}: |
| ip_range = obj['ip_range'].get('cidr', None) |
| else: |
| group = obj['group'].get('name', None) |
| tenant_id = obj['group'].get('tenant_id', None) |
| |
| return OpenStackSecurityGroupRule( |
| id=obj['id'], parent_group_id=obj['parent_group_id'], |
| ip_protocol=obj['ip_protocol'], from_port=obj['from_port'], |
| to_port=obj['to_port'], driver=self, ip_range=ip_range, |
| group=group, tenant_id=tenant_id) |
| |
| def _to_security_groups(self, obj): |
| security_groups = obj['security_groups'] |
| return [self._to_security_group(security_group) for security_group in |
| security_groups] |
| |
| def _to_security_group(self, obj): |
| rules = self._to_security_group_rules(obj.get('security_group_rules', |
| obj.get('rules', []))) |
| return OpenStackSecurityGroup(id=obj['id'], |
| tenant_id=obj['tenant_id'], |
| name=obj['name'], |
| description=obj.get('description', ''), |
| rules=rules, |
| driver=self) |
| |
| def ex_list_security_groups(self): |
| """ |
| Get a list of Security Groups that are available. |
| |
| :rtype: ``list`` of :class:`OpenStackSecurityGroup` |
| """ |
| return self._to_security_groups( |
| self.connection.request('/os-security-groups').object) |
| |
| def ex_get_node_security_groups(self, node): |
| """ |
| Get Security Groups of the specified server. |
| |
| :rtype: ``list`` of :class:`OpenStackSecurityGroup` |
| """ |
| return self._to_security_groups( |
| self.connection.request('/servers/%s/os-security-groups' % |
| (node.id)).object) |
| |
| def ex_create_security_group(self, name, description): |
| """ |
| Create a new Security Group |
| |
| :param name: Name of the new Security Group |
| :type name: ``str`` |
| |
| :param description: Description of the new Security Group |
| :type description: ``str`` |
| |
| :rtype: :class:`OpenStackSecurityGroup` |
| """ |
| return self._to_security_group(self.connection.request( |
| '/os-security-groups', method='POST', |
| data={'security_group': {'name': name, 'description': description}} |
| ).object['security_group']) |
| |
| def ex_delete_security_group(self, security_group): |
| """ |
| Delete a Security Group. |
| |
| :param security_group: Security Group should be deleted |
| :type security_group: :class:`OpenStackSecurityGroup` |
| |
| :rtype: ``bool`` |
| """ |
| resp = self.connection.request('/os-security-groups/%s' % |
| (security_group.id), |
| method='DELETE') |
| return resp.status in (httplib.NO_CONTENT, httplib.ACCEPTED) |
| |
| def ex_create_security_group_rule(self, security_group, ip_protocol, |
| from_port, to_port, cidr=None, |
| source_security_group=None): |
| """ |
| Create a new Rule in a Security Group |
| |
| :param security_group: Security Group in which to add the rule |
| :type security_group: :class:`OpenStackSecurityGroup` |
| |
| :param ip_protocol: Protocol to which this rule applies |
| Examples: tcp, udp, ... |
| :type ip_protocol: ``str`` |
| |
| :param from_port: First port of the port range |
| :type from_port: ``int`` |
| |
| :param to_port: Last port of the port range |
| :type to_port: ``int`` |
| |
| :param cidr: CIDR notation of the source IP range for this rule |
| :type cidr: ``str`` |
| |
| :param source_security_group: Existing Security Group to use as the |
| source (instead of CIDR) |
| :type source_security_group: L{OpenStackSecurityGroup |
| |
| :rtype: :class:`OpenStackSecurityGroupRule` |
| """ |
| source_security_group_id = None |
| if type(source_security_group) == OpenStackSecurityGroup: |
| source_security_group_id = source_security_group.id |
| |
| return self._to_security_group_rule(self.connection.request( |
| '/os-security-group-rules', method='POST', |
| data={'security_group_rule': { |
| 'ip_protocol': ip_protocol, |
| 'from_port': from_port, |
| 'to_port': to_port, |
| 'cidr': cidr, |
| 'group_id': source_security_group_id, |
| 'parent_group_id': security_group.id}} |
| ).object['security_group_rule']) |
| |
| def ex_delete_security_group_rule(self, rule): |
| """ |
| Delete a Rule from a Security Group. |
| |
| :param rule: Rule should be deleted |
| :type rule: :class:`OpenStackSecurityGroupRule` |
| |
| :rtype: ``bool`` |
| """ |
| resp = self.connection.request('/os-security-group-rules/%s' % |
| (rule.id), method='DELETE') |
| return resp.status == httplib.NO_CONTENT |
| |
| def _to_key_pairs(self, obj): |
| key_pairs = obj['keypairs'] |
| key_pairs = [self._to_key_pair(key_pair['keypair']) for key_pair in |
| key_pairs] |
| return key_pairs |
| |
| def _to_key_pair(self, obj): |
| key_pair = KeyPair(name=obj['name'], |
| fingerprint=obj['fingerprint'], |
| public_key=obj['public_key'], |
| private_key=obj.get('private_key', None), |
| driver=self) |
| return key_pair |
| |
| def list_key_pairs(self): |
| response = self.connection.request('/os-keypairs') |
| key_pairs = self._to_key_pairs(response.object) |
| return key_pairs |
| |
| def get_key_pair(self, name): |
| self.connection.set_context({'key_pair_name': name}) |
| |
| response = self.connection.request('/os-keypairs/%s' % (name)) |
| key_pair = self._to_key_pair(response.object['keypair']) |
| return key_pair |
| |
| def create_key_pair(self, name): |
| data = {'keypair': {'name': name}} |
| response = self.connection.request('/os-keypairs', method='POST', |
| data=data) |
| key_pair = self._to_key_pair(response.object['keypair']) |
| return key_pair |
| |
| def import_key_pair_from_string(self, name, key_material): |
| data = {'keypair': {'name': name, 'public_key': key_material}} |
| response = self.connection.request('/os-keypairs', method='POST', |
| data=data) |
| key_pair = self._to_key_pair(response.object['keypair']) |
| return key_pair |
| |
| def delete_key_pair(self, key_pair): |
| """ |
| Delete a KeyPair. |
| |
| :param keypair: KeyPair to delete |
| :type keypair: :class:`OpenStackKeyPair` |
| |
| :rtype: ``bool`` |
| """ |
| response = self.connection.request('/os-keypairs/%s' % (key_pair.name), |
| method='DELETE') |
| return response.status == httplib.ACCEPTED |
| |
| def ex_list_keypairs(self): |
| """ |
| Get a list of KeyPairs that are available. |
| |
| :rtype: ``list`` of :class:`OpenStackKeyPair` |
| """ |
| warnings.warn('This method has been deprecated in favor of ' |
| 'list_key_pairs method') |
| |
| return self.list_key_pairs() |
| |
| def ex_create_keypair(self, name): |
| """ |
| Create a new KeyPair |
| |
| :param name: Name of the new KeyPair |
| :type name: ``str`` |
| |
| :rtype: :class:`OpenStackKeyPair` |
| """ |
| warnings.warn('This method has been deprecated in favor of ' |
| 'create_key_pair method') |
| |
| return self.create_key_pair(name=name) |
| |
| def ex_import_keypair(self, name, keyfile): |
| """ |
| Import a KeyPair from a file |
| |
| :param name: Name of the new KeyPair |
| :type name: ``str`` |
| |
| :param keyfile: Path to the public key file (in OpenSSH format) |
| :type keyfile: ``str`` |
| |
| :rtype: :class:`OpenStackKeyPair` |
| """ |
| warnings.warn('This method has been deprecated in favor of ' |
| 'import_key_pair_from_file method') |
| |
| return self.import_key_pair_from_file(name=name, key_file_path=keyfile) |
| |
| def ex_import_keypair_from_string(self, name, key_material): |
| """ |
| Import a KeyPair from a string |
| |
| :param name: Name of the new KeyPair |
| :type name: ``str`` |
| |
| :param key_material: Public key (in OpenSSH format) |
| :type key_material: ``str`` |
| |
| :rtype: :class:`OpenStackKeyPair` |
| """ |
| warnings.warn('This method has been deprecated in favor of ' |
| 'import_key_pair_from_string method') |
| |
| return self.import_key_pair_from_string(name=name, |
| key_material=key_material) |
| |
| def ex_delete_keypair(self, keypair): |
| """ |
| Delete a KeyPair. |
| |
| :param keypair: KeyPair to delete |
| :type keypair: :class:`OpenStackKeyPair` |
| |
| :rtype: ``bool`` |
| """ |
| warnings.warn('This method has been deprecated in favor of ' |
| 'delete_key_pair method') |
| |
| return self.delete_key_pair(key_pair=keypair) |
| |
| def ex_get_size(self, size_id): |
| """ |
| Get a NodeSize |
| |
| :param size_id: ID of the size which should be used |
| :type size_id: ``str`` |
| |
| :rtype: :class:`NodeSize` |
| """ |
| return self._to_size(self.connection.request( |
| '/flavors/%s' % (size_id,)) .object['flavor']) |
| |
| def ex_get_size_extra_specs(self, size_id): |
| """ |
| Get the extra_specs field of a NodeSize |
| |
| :param size_id: ID of the size which should be used |
| :type size_id: ``str`` |
| |
| :rtype: `dict` |
| """ |
| return self.connection.request( |
| '/flavors/%s/os-extra_specs' % (size_id,)) .object['extra_specs'] |
| |
| def get_image(self, image_id): |
| """ |
| Get a NodeImage |
| |
| @inherits: :class:`NodeDriver.get_image` |
| |
| :param image_id: ID of the image which should be used |
| :type image_id: ``str`` |
| |
| :rtype: :class:`NodeImage` |
| """ |
| return self._to_image(self.connection.request( |
| '/images/%s' % (image_id,)).object['image']) |
| |
| def delete_image(self, image): |
| """ |
| Delete a NodeImage |
| |
| @inherits: :class:`NodeDriver.delete_image` |
| |
| :param image: image witch should be used |
| :type image: :class:`NodeImage` |
| |
| :rtype: ``bool`` |
| """ |
| resp = self.connection.request('/images/%s' % (image.id,), |
| method='DELETE') |
| return resp.status == httplib.NO_CONTENT |
| |
| def _node_action(self, node, action, **params): |
| params = params or None |
| return self.connection.request('/servers/%s/action' % (node.id,), |
| method='POST', data={action: params}) |
| |
| def _update_node(self, node, **node_updates): |
| """ |
| Updates the editable attributes of a server, which currently include |
| its name and IPv4/IPv6 access addresses. |
| """ |
| return self._to_node( |
| self.connection.request( |
| '/servers/%s' % (node.id,), method='PUT', |
| data={'server': node_updates} |
| ).object['server'] |
| ) |
| |
| def _to_node_from_obj(self, obj): |
| return self._to_node(obj['server']) |
| |
| def _to_node(self, api_node): |
| public_networks_labels = ['public', 'internet'] |
| |
| public_ips, private_ips = [], [] |
| |
| for label, values in api_node['addresses'].items(): |
| for value in values: |
| ip = value['addr'] |
| is_public_ip = False |
| |
| try: |
| is_public_ip = is_public_subnet(ip) |
| except Exception: |
| # IPv6 |
| |
| # Openstack Icehouse sets 'OS-EXT-IPS:type' to 'floating' |
| # for public and 'fixed' for private |
| explicit_ip_type = value.get('OS-EXT-IPS:type', None) |
| |
| if label in public_networks_labels: |
| is_public_ip = True |
| elif explicit_ip_type == 'floating': |
| is_public_ip = True |
| elif explicit_ip_type == 'fixed': |
| is_public_ip = False |
| |
| if is_public_ip: |
| public_ips.append(ip) |
| else: |
| private_ips.append(ip) |
| |
| # Sometimes 'image' attribute is not present if the node is in an error |
| # state |
| image = api_node.get('image', None) |
| image_id = image.get('id', None) if image else None |
| config_drive = api_node.get("config_drive", False) |
| volumes_attached = api_node.get('os-extended-volumes:volumes_attached') |
| created = parse_date(api_node["created"]) |
| |
| return Node( |
| id=api_node['id'], |
| name=api_node['name'], |
| state=self.NODE_STATE_MAP.get(api_node['status'], |
| NodeState.UNKNOWN), |
| public_ips=public_ips, |
| private_ips=private_ips, |
| created_at=created, |
| driver=self, |
| extra=dict( |
| addresses=api_node['addresses'], |
| hostId=api_node['hostId'], |
| access_ip=api_node.get('accessIPv4'), |
| access_ipv6=api_node.get('accessIPv6', None), |
| # Docs says "tenantId", but actual is "tenant_id". *sigh* |
| # Best handle both. |
| tenantId=api_node.get('tenant_id') or api_node['tenantId'], |
| userId=api_node.get('user_id', None), |
| imageId=image_id, |
| flavorId=api_node['flavor']['id'], |
| uri=next(link['href'] for link in api_node['links'] if |
| link['rel'] == 'self'), |
| # pylint: disable=no-member |
| service_name=self.connection.get_service_name(), |
| metadata=api_node['metadata'], |
| password=api_node.get('adminPass', None), |
| created=api_node['created'], |
| updated=api_node['updated'], |
| key_name=api_node.get('key_name', None), |
| disk_config=api_node.get('OS-DCF:diskConfig', None), |
| config_drive=config_drive, |
| availability_zone=api_node.get('OS-EXT-AZ:availability_zone'), |
| volumes_attached=volumes_attached, |
| task_state=api_node.get("OS-EXT-STS:task_state", None), |
| vm_state=api_node.get("OS-EXT-STS:vm_state", None), |
| power_state=api_node.get("OS-EXT-STS:power_state", None), |
| progress=api_node.get("progress", None), |
| fault=api_node.get('fault') |
| ), |
| ) |
| |
| def _to_volume(self, api_node): |
| if 'volume' in api_node: |
| api_node = api_node['volume'] |
| |
| state = self.VOLUME_STATE_MAP.get(api_node['status'], |
| StorageVolumeState.UNKNOWN) |
| |
| return StorageVolume( |
| id=api_node['id'], |
| name=api_node.get('displayName', api_node.get('name')), |
| size=api_node['size'], |
| state=state, |
| driver=self, |
| extra={ |
| 'description': api_node.get('displayDescription', |
| api_node.get('description')), |
| 'attachments': [att for att in api_node['attachments'] if att], |
| # TODO: remove in 1.18.0 |
| 'state': api_node.get('status', None), |
| 'snapshot_id': api_node.get('snapshot_id', |
| api_node.get('snapshotId')), |
| 'location': api_node.get('availability_zone', |
| api_node.get('availabilityZone')), |
| 'volume_type': api_node.get('volume_type', |
| api_node.get('volumeType')), |
| 'metadata': api_node.get('metadata', None), |
| 'created_at': api_node.get('created_at', |
| api_node.get('createdAt')) |
| } |
| ) |
| |
| def _to_snapshot(self, data): |
| if 'snapshot' in data: |
| data = data['snapshot'] |
| |
| volume_id = data.get('volume_id', data.get('volumeId', None)) |
| display_name = data.get('name', |
| data.get('display_name', |
| data.get('displayName', None))) |
| created_at = data.get('created_at', data.get('createdAt', None)) |
| description = data.get('description', |
| data.get('display_description', |
| data.get('displayDescription', None))) |
| status = data.get('status', None) |
| |
| extra = {'volume_id': volume_id, |
| 'name': display_name, |
| 'created': created_at, |
| 'description': description, |
| 'status': status} |
| |
| state = self.SNAPSHOT_STATE_MAP.get( |
| status, |
| VolumeSnapshotState.UNKNOWN |
| ) |
| |
| try: |
| created_dt = parse_date(created_at) |
| except ValueError: |
| created_dt = None |
| |
| snapshot = VolumeSnapshot(id=data['id'], driver=self, |
| size=data['size'], extra=extra, |
| created=created_dt, state=state, |
| name=display_name) |
| return snapshot |
| |
| def _to_size(self, api_flavor, price=None, bandwidth=None): |
| # if provider-specific subclasses can get better values for |
| # price/bandwidth, then can pass them in when they super(). |
| if not price: |
| price = self._get_size_price(str(api_flavor['id'])) |
| |
| extra = api_flavor.get('OS-FLV-WITH-EXT-SPECS:extra_specs', {}) |
| return OpenStackNodeSize( |
| id=api_flavor['id'], |
| name=api_flavor['name'], |
| ram=api_flavor['ram'], |
| disk=api_flavor['disk'], |
| vcpus=api_flavor['vcpus'], |
| ephemeral_disk=api_flavor.get('OS-FLV-EXT-DATA:ephemeral', None), |
| swap=api_flavor['swap'], |
| extra=extra, |
| bandwidth=bandwidth, |
| price=price, |
| driver=self, |
| ) |
| |
| def _get_size_price(self, size_id): |
| try: |
| return get_size_price( |
| driver_type='compute', |
| driver_name=self.api_name, |
| size_id=size_id, |
| ) |
| except KeyError: |
| return(0.0) |
| |
| def _extract_image_id_from_url(self, location_header): |
| path = urlparse.urlparse(location_header).path |
| image_id = path.split('/')[-1] |
| return image_id |
| |
| def ex_rescue(self, node, password=None): |
| # Requires Rescue Mode extension |
| """ |
| Rescue a node |
| |
| :param node: node |
| :type node: :class:`Node` |
| |
| :param password: password |
| :type password: ``str`` |
| |
| :rtype: :class:`Node` |
| """ |
| if password: |
| resp = self._node_action(node, 'rescue', adminPass=password) |
| else: |
| resp = self._node_action(node, 'rescue') |
| password = json.loads(resp.body)['adminPass'] |
| node.extra['password'] = password |
| return node |
| |
| def ex_unrescue(self, node): |
| """ |
| Unrescue a node |
| |
| :param node: node |
| :type node: :class:`Node` |
| |
| :rtype: ``bool`` |
| """ |
| resp = self._node_action(node, 'unrescue') |
| return resp.status == httplib.ACCEPTED |
| |
| def _to_floating_ip_pools(self, obj): |
| pool_elements = obj['floating_ip_pools'] |
| return [self._to_floating_ip_pool(pool) for pool in pool_elements] |
| |
| def _to_floating_ip_pool(self, obj): |
| return OpenStack_1_1_FloatingIpPool(obj['name'], self.connection) |
| |
| def ex_list_floating_ip_pools(self): |
| """ |
| List available floating IP pools |
| |
| :rtype: ``list`` of :class:`OpenStack_1_1_FloatingIpPool` |
| """ |
| return self._to_floating_ip_pools( |
| self.connection.request('/os-floating-ip-pools').object) |
| |
| def _to_floating_ips(self, obj): |
| ip_elements = obj['floating_ips'] |
| return [self._to_floating_ip(ip) for ip in ip_elements] |
| |
| def _to_floating_ip(self, obj): |
| return OpenStack_1_1_FloatingIpAddress(id=obj['id'], |
| ip_address=obj['ip'], |
| pool=None, |
| node_id=obj['instance_id'], |
| driver=self) |
| |
| def ex_list_floating_ips(self): |
| """ |
| List floating IPs |
| |
| :rtype: ``list`` of :class:`OpenStack_1_1_FloatingIpAddress` |
| """ |
| return self._to_floating_ips( |
| self.connection.request('/os-floating-ips').object) |
| |
| def ex_get_floating_ip(self, ip): |
| """ |
| Get specified floating IP |
| |
| :param ip: floating IP to get |
| :type ip: ``str`` |
| |
| :rtype: :class:`OpenStack_1_1_FloatingIpAddress` |
| """ |
| floating_ips = self.ex_list_floating_ips() |
| ip_obj, = [x for x in floating_ips if x.ip_address == ip] |
| return ip_obj |
| |
| def ex_create_floating_ip(self, ip_pool=None): |
| """ |
| Create new floating IP. The ip_pool attribute is optional only if your |
| infrastructure has only one IP pool available. |
| |
| :param ip_pool: name of the floating IP pool |
| :type ip_pool: ``str`` |
| |
| :rtype: :class:`OpenStack_1_1_FloatingIpAddress` |
| """ |
| data = {'pool': ip_pool} if ip_pool is not None else {} |
| resp = self.connection.request('/os-floating-ips', |
| method='POST', |
| data=data) |
| |
| data = resp.object['floating_ip'] |
| id = data['id'] |
| ip_address = data['ip'] |
| return OpenStack_1_1_FloatingIpAddress(id=id, |
| ip_address=ip_address, |
| pool=None, |
| node_id=None, |
| driver=self) |
| |
| def ex_delete_floating_ip(self, ip): |
| """ |
| Delete specified floating IP |
| |
| :param ip: floating IP to remove |
| :type ip: :class:`OpenStack_1_1_FloatingIpAddress` |
| |
| :rtype: ``bool`` |
| """ |
| resp = self.connection.request('/os-floating-ips/%s' % ip.id, |
| method='DELETE') |
| return resp.status in (httplib.NO_CONTENT, httplib.ACCEPTED) |
| |
| def ex_attach_floating_ip_to_node(self, node, ip): |
| """ |
| Attach the floating IP to the node |
| |
| :param node: node |
| :type node: :class:`Node` |
| |
| :param ip: floating IP to attach |
| :type ip: ``str`` or :class:`OpenStack_1_1_FloatingIpAddress` |
| |
| :rtype: ``bool`` |
| """ |
| address = ip.ip_address if hasattr(ip, 'ip_address') else ip |
| data = { |
| 'addFloatingIp': {'address': address} |
| } |
| resp = self.connection.request('/servers/%s/action' % node.id, |
| method='POST', data=data) |
| return resp.status == httplib.ACCEPTED |
| |
| def ex_detach_floating_ip_from_node(self, node, ip): |
| """ |
| Detach the floating IP from the node |
| |
| :param node: node |
| :type node: :class:`Node` |
| |
| :param ip: floating IP to remove |
| :type ip: ``str`` or :class:`OpenStack_1_1_FloatingIpAddress` |
| |
| :rtype: ``bool`` |
| """ |
| address = ip.ip_address if hasattr(ip, 'ip_address') else ip |
| data = { |
| 'removeFloatingIp': {'address': address} |
| } |
| resp = self.connection.request('/servers/%s/action' % node.id, |
| method='POST', data=data) |
| return resp.status == httplib.ACCEPTED |
| |
| def ex_get_metadata_for_node(self, node): |
| """ |
| Return the metadata associated with the node. |
| |
| :param node: Node instance |
| :type node: :class:`Node` |
| |
| :return: A dictionary or other mapping of strings to strings, |
| associating tag names with tag values. |
| :type tags: ``dict`` |
| """ |
| return node.extra['metadata'] |
| |
| def ex_pause_node(self, node): |
| return self._post_simple_node_action(node, 'pause') |
| |
| def ex_unpause_node(self, node): |
| return self._post_simple_node_action(node, 'unpause') |
| |
| def ex_start_node(self, node): |
| # NOTE: This method is here for backward compatibility reasons after |
| # this method was promoted to be part of the standard compute API in |
| # Libcloud v2.7.0 |
| return self.start_node(node=node) |
| |
| def ex_stop_node(self, node): |
| # NOTE: This method is here for backward compatibility reasons after |
| # this method was promoted to be part of the standard compute API in |
| # Libcloud v2.7.0 |
| return self.stop_node(node=node) |
| |
| def ex_suspend_node(self, node): |
| return self._post_simple_node_action(node, 'suspend') |
| |
| def ex_resume_node(self, node): |
| return self._post_simple_node_action(node, 'resume') |
| |
| def _post_simple_node_action(self, node, action): |
| """ Post a simple, data-less action to the OS node action endpoint |
| :param `Node` node: |
| :param str action: the action to call |
| :return `bool`: a boolean that indicates success |
| """ |
| uri = '/servers/{node_id}/action'.format(node_id=node.id) |
| resp = self.connection.request(uri, method='POST', data={action: None}) |
| return resp.status == httplib.ACCEPTED |
| |
| |
| class OpenStack_2_Connection(OpenStackComputeConnection): |
| responseCls = OpenStack_1_1_Response |
| accept_format = 'application/json' |
| default_content_type = 'application/json; charset=UTF-8' |
| |
| def encode_data(self, data): |
| return json.dumps(data) |
| |
| |
| class OpenStack_2_ImageConnection(OpenStackImageConnection): |
| responseCls = OpenStack_1_1_Response |
| accept_format = 'application/json' |
| default_content_type = 'application/json; charset=UTF-8' |
| |
| def encode_data(self, data): |
| return json.dumps(data) |
| |
| |
| class OpenStack_2_NetworkConnection(OpenStackNetworkConnection): |
| responseCls = OpenStack_1_1_Response |
| accept_format = 'application/json' |
| default_content_type = 'application/json; charset=UTF-8' |
| |
| def encode_data(self, data): |
| return json.dumps(data) |
| |
| |
| class OpenStack_2_VolumeV2Connection(OpenStackVolumeV2Connection): |
| responseCls = OpenStack_1_1_Response |
| accept_format = 'application/json' |
| default_content_type = 'application/json; charset=UTF-8' |
| |
| def encode_data(self, data): |
| return json.dumps(data) |
| |
| |
| class OpenStack_2_PortInterfaceState(Type): |
| """ |
| Standard states of OpenStack_2_PortInterfaceState |
| """ |
| BUILD = 'build' |
| ACTIVE = 'active' |
| DOWN = 'down' |
| UNKNOWN = 'unknown' |
| |
| |
| class OpenStack_2_NodeDriver(OpenStack_1_1_NodeDriver): |
| """ |
| OpenStack node driver. |
| """ |
| connectionCls = OpenStack_2_Connection |
| |
| # Previously all image functionality was available through the |
| # compute API. This deprecated proxied API does not offer all |
| # functionality that the Glance Image service API offers. |
| # See https://developer.openstack.org/api-ref/compute/ |
| # |
| # > These APIs are proxy calls to the Image service. Nova has deprecated |
| # > all the proxy APIs and users should use the native APIs instead. These |
| # > will fail with a 404 starting from microversion 2.36. See: Relevant |
| # > Image APIs. |
| # |
| # For example, managing image visibility and sharing machine |
| # images across tenants can not be done using the proxied image API in the |
| # compute endpoint, but it can be done with the Glance Image API. |
| # See https://developer.openstack.org/api-ref/ |
| # image/v2/index.html#list-image-members |
| image_connectionCls = OpenStack_2_ImageConnection |
| image_connection = None |
| |
| # Similarly not all node-related operations are exposed through the |
| # compute API |
| # See https://developer.openstack.org/api-ref/compute/ |
| # For example, creating a new node in an OpenStack that is configured to |
| # create a new port for every new instance will make it so that if that |
| # port is detached it disappears. But if the port is manually created |
| # beforehand using the neutron network API and node is booted with that |
| # port pre-specified, then detaching that port later will result in that |
| # becoming a re-attachable resource much like a floating ip. So because |
| # even though this is the compute driver, we do connect to the networking |
| # API here because some operations relevant for compute can only be |
| # accessed from there. |
| network_connectionCls = OpenStack_2_NetworkConnection |
| network_connection = None |
| |
| # Similarly all image operations are noe exposed through the block-storage |
| # API of the cinder service: |
| # https://developer.openstack.org/api-ref/block-storage/ |
| volumev2_connectionCls = OpenStack_2_VolumeV2Connection |
| volumev2_connection = None |
| |
| type = Provider.OPENSTACK |
| |
| features = {"create_node": ["generates_password"]} |
| _networks_url_prefix = '/v2.0/networks' |
| _subnets_url_prefix = '/v2.0/subnets' |
| |
| PORT_INTERFACE_MAP = { |
| 'BUILD': OpenStack_2_PortInterfaceState.BUILD, |
| 'ACTIVE': OpenStack_2_PortInterfaceState.ACTIVE, |
| 'DOWN': OpenStack_2_PortInterfaceState.DOWN, |
| 'UNKNOWN': OpenStack_2_PortInterfaceState.UNKNOWN |
| } |
| |
| def __init__(self, *args, **kwargs): |
| original_connectionCls = self.connectionCls |
| self._ex_force_api_version = str(kwargs.pop('ex_force_api_version', |
| None)) |
| if 'ex_force_auth_version' not in kwargs: |
| kwargs['ex_force_auth_version'] = '3.x_password' |
| |
| original_ex_force_base_url = kwargs.get('ex_force_base_url') |
| |
| # We run the init once to get the Glance V2 API connection |
| # and put that on the object under self.image_connection. |
| if original_ex_force_base_url or kwargs.get('ex_force_image_url'): |
| kwargs['ex_force_base_url'] = \ |
| str(kwargs.pop('ex_force_image_url', |
| original_ex_force_base_url)) |
| self.connectionCls = self.image_connectionCls |
| super(OpenStack_2_NodeDriver, self).__init__(*args, **kwargs) |
| self.image_connection = self.connection |
| |
| # We run the init once to get the Cinder V2 API connection |
| # and put that on the object under self.volumev2_connection. |
| if original_ex_force_base_url or kwargs.get('ex_force_volume_url'): |
| kwargs['ex_force_base_url'] = \ |
| str(kwargs.pop('ex_force_volume_url', |
| original_ex_force_base_url)) |
| self.connectionCls = self.volumev2_connectionCls |
| super(OpenStack_2_NodeDriver, self).__init__(*args, **kwargs) |
| self.volumev2_connection = self.connection |
| |
| # We run the init once to get the Neutron V2 API connection |
| # and put that on the object under self.network_connection. |
| if original_ex_force_base_url or kwargs.get('ex_force_network_url'): |
| kwargs['ex_force_base_url'] = \ |
| str(kwargs.pop('ex_force_network_url', |
| original_ex_force_base_url)) |
| self.connectionCls = self.network_connectionCls |
| super(OpenStack_2_NodeDriver, self).__init__(*args, **kwargs) |
| self.network_connection = self.connection |
| |
| # We run the init once again to get the compute API connection |
| # and that's put under self.connection as normal. |
| self._ex_force_base_url = original_ex_force_base_url |
| if original_ex_force_base_url: |
| kwargs['ex_force_base_url'] = self._ex_force_base_url |
| # if ex_force_base_url is not set in original params delete it |
| elif 'ex_force_base_url' in kwargs: |
| del kwargs['ex_force_base_url'] |
| self.connectionCls = original_connectionCls |
| super(OpenStack_2_NodeDriver, self).__init__(*args, **kwargs) |
| |
| def _to_port(self, element): |
| created = element.get('created_at') |
| updated = element.get('updated_at') |
| return OpenStack_2_PortInterface( |
| id=element['id'], |
| state=self.PORT_INTERFACE_MAP.get( |
| element.get('status'), OpenStack_2_PortInterfaceState.UNKNOWN |
| ), |
| created=created, |
| driver=self, |
| extra=dict( |
| admin_state_up=element['admin_state_up'], |
| allowed_address_pairs=element['allowed_address_pairs'], |
| binding_vnic_type=element['binding:vnic_type'], |
| device_id=element['device_id'], |
| description=element['description'], |
| device_owner=element['device_owner'], |
| fixed_ips=element['fixed_ips'], |
| mac_address=element['mac_address'], |
| name=element['name'], |
| network_id=element['network_id'], |
| project_id=element.get('project_id', None), |
| port_security_enabled=element.get('port_security_enabled', |
| None), |
| revision_number=element.get('revision_number', None), |
| security_groups=element['security_groups'], |
| tags=element.get('tags', None), |
| tenant_id=element['tenant_id'], |
| updated=updated, |
| ) |
| ) |
| |
| def list_nodes(self, ex_all_tenants=False): |
| """ |
| List the nodes in a tenant |
| |
| :param ex_all_tenants: List nodes for all the tenants. Note: Your user |
| must have admin privileges for this |
| functionality to work. |
| :type ex_all_tenants: ``bool`` |
| """ |
| params = {} |
| if ex_all_tenants: |
| params = {'all_tenants': 1} |
| return self._to_nodes(self._paginated_request( |
| '/servers/detail', 'servers', self.connection, params=params)) |
| |
| def get_image(self, image_id): |
| """ |
| Get a NodeImage using the V2 Glance API |
| |
| @inherits: :class:`OpenStack_1_1_NodeDriver.get_image` |
| |
| :param image_id: ID of the image which should be used |
| :type image_id: ``str`` |
| |
| :rtype: :class:`NodeImage` |
| """ |
| return self._to_image(self.image_connection.request( |
| '/v2/images/%s' % (image_id,)).object) |
| |
| def list_images(self, location=None, ex_only_active=True): |
| """ |
| Lists all active images using the V2 Glance API |
| |
| @inherits: :class:`NodeDriver.list_images` |
| |
| :param location: Which data center to list the images in. If |
| empty, undefined behavior will be selected. |
| (optional) |
| :type location: :class:`.NodeLocation` |
| |
| :param ex_only_active: True if list only active (optional) |
| :type ex_only_active: ``bool`` |
| """ |
| if location is not None: |
| raise NotImplementedError( |
| "location in list_images is not implemented " |
| "in the OpenStack_2_NodeDriver") |
| if not ex_only_active: |
| raise NotImplementedError( |
| "ex_only_active in list_images is not implemented " |
| "in the OpenStack_2_NodeDriver") |
| |
| result = self._paginated_request_next( |
| path='/v2/images', |
| request_method=self.image_connection.request, |
| response_key='images') |
| |
| images = [] |
| for item in result: |
| images.append(self._to_image(item)) |
| |
| return images |
| |
| def ex_update_image(self, image_id, data): |
| """ |
| Patch a NodeImage. Can be used to set visibility |
| |
| :param image_id: ID of the image which should be used |
| :type image_id: ``str`` |
| |
| :param data: The data to PATCH, either a dict or a list |
| for example: [ |
| {'op': 'replace', 'path': '/visibility', 'value': 'shared'} |
| ] |
| :type data: ``dict|list`` |
| |
| :rtype: :class:`NodeImage` |
| """ |
| response = self.image_connection.request( |
| '/v2/images/%s' % (image_id,), |
| headers={'Content-type': 'application/' |
| 'openstack-images-' |
| 'v2.1-json-patch'}, |
| method='PATCH', data=data |
| ) |
| return self._to_image(response.object) |
| |
| def ex_list_image_members(self, image_id): |
| """ |
| List all members of an image. See |
| https://developer.openstack.org/api-ref/image/v2/index.html#sharing |
| |
| :param image_id: ID of the image of which the members should |
| be listed |
| :type image_id: ``str`` |
| |
| :rtype: ``list`` of :class:`NodeImageMember` |
| """ |
| response = self.image_connection.request( |
| '/v2/images/%s/members' % (image_id,) |
| ) |
| image_members = [] |
| for image_member in response.object['members']: |
| image_members.append(self._to_image_member(image_member)) |
| return image_members |
| |
| def ex_create_image_member(self, image_id, member_id): |
| """ |
| Give a project access to an image. |
| |
| The image should have visibility status 'shared'. |
| |
| Note that this is not an idempotent operation. If this action is |
| attempted using a tenant that is already in the image members |
| group the API will throw a Conflict (409). |
| See the 'create-image-member' section on |
| https://developer.openstack.org/api-ref/image/v2/index.html |
| |
| :param str image_id: The ID of the image to share with the specified |
| tenant |
| :param str member_id: The ID of the project / tenant (the image member) |
| Note that this is the Keystone project ID and not the project name, |
| so something like e2151b1fe02d4a8a2d1f5fc331522c0a |
| :return None: |
| |
| :param image_id: ID of the image to share |
| :type image_id: ``str`` |
| |
| :param project: ID of the project to give access to the image |
| :type image_id: ``str`` |
| |
| :rtype: ``list`` of :class:`NodeImageMember` |
| """ |
| data = {'member': member_id} |
| response = self.image_connection.request( |
| '/v2/images/%s/members' % image_id, |
| method='POST', data=data |
| ) |
| return self._to_image_member(response.object) |
| |
| def ex_get_image_member(self, image_id, member_id): |
| """ |
| Get a member of an image by id |
| |
| :param image_id: ID of the image of which the member should |
| be listed |
| :type image_id: ``str`` |
| |
| :param member_id: ID of the member to list |
| :type image_id: ``str`` |
| |
| :rtype: ``list`` of :class:`NodeImageMember` |
| """ |
| response = self.image_connection.request( |
| '/v2/images/%s/members/%s' % (image_id, member_id) |
| ) |
| return self._to_image_member(response.object) |
| |
| def ex_accept_image_member(self, image_id, member_id): |
| """ |
| Accept a pending image as a member. |
| |
| This call is idempotent unlike ex_create_image_member, |
| you can accept the same image many times. |
| |
| :param image_id: ID of the image to accept |
| :type image_id: ``str`` |
| |
| :param project: ID of the project to accept the image as |
| :type image_id: ``str`` |
| |
| :rtype: ``bool`` |
| """ |
| data = {'status': 'accepted'} |
| response = self.image_connection.request( |
| '/v2/images/%s/members/%s' % (image_id, member_id), |
| method='PUT', data=data |
| ) |
| return self._to_image_member(response.object) |
| |
| def _to_networks(self, obj): |
| networks = obj['networks'] |
| return [self._to_network(network) for network in networks] |
| |
| def _to_network(self, obj): |
| extra = {} |
| if obj.get('router:external', None): |
| extra['router:external'] = obj.get('router:external') |
| if obj.get('subnets', None): |
| extra['subnets'] = obj.get('subnets') |
| return OpenStackNetwork(id=obj['id'], |
| name=obj['name'], |
| cidr=None, |
| driver=self, |
| extra=extra) |
| |
| def ex_list_networks(self): |
| """ |
| Get a list of Networks that are available. |
| |
| :rtype: ``list`` of :class:`OpenStackNetwork` |
| """ |
| response = self.network_connection.request( |
| self._networks_url_prefix).object |
| return self._to_networks(response) |
| |
| def ex_get_network(self, network_id): |
| """ |
| Retrieve the Network with the given ID |
| |
| :param networkId: ID of the network |
| :type networkId: ``str`` |
| |
| :rtype :class:`OpenStackNetwork` |
| """ |
| request_url = "{networks_url_prefix}/{network_id}".format( |
| networks_url_prefix=self._networks_url_prefix, |
| network_id=network_id |
| ) |
| response = self.network_connection.request(request_url).object |
| return self._to_network(response['network']) |
| |
| def ex_create_network(self, name, **kwargs): |
| """ |
| Create a new Network |
| |
| :param name: Name of network which should be used |
| :type name: ``str`` |
| |
| :rtype: :class:`OpenStackNetwork` |
| """ |
| data = {'network': {'name': name}} |
| # Add optional values |
| for key, value in kwargs.items(): |
| data['network'][key] = value |
| response = self.network_connection.request(self._networks_url_prefix, |
| method='POST', |
| data=data).object |
| return self._to_network(response['network']) |
| |
| def ex_delete_network(self, network): |
| """ |
| Delete a Network |
| |
| :param network: Network which should be used |
| :type network: :class:`OpenStackNetwork` |
| |
| :rtype: ``bool`` |
| """ |
| resp = self.network_connection.request( |
| '%s/%s' % (self._networks_url_prefix, |
| network.id), method='DELETE') |
| return resp.status in (httplib.NO_CONTENT, httplib.ACCEPTED) |
| |
| def _to_subnets(self, obj): |
| subnets = obj['subnets'] |
| return [self._to_subnet(subnet) for subnet in subnets] |
| |
| def _to_subnet(self, obj): |
| extra = {} |
| if obj.get('router:external', None): |
| extra['router:external'] = obj.get('router:external') |
| if obj.get('subnets', None): |
| extra['subnets'] = obj.get('subnets') |
| return OpenStack_2_SubNet(id=obj['id'], |
| name=obj['name'], |
| cidr=obj['cidr'], |
| network_id=obj['network_id'], |
| driver=self, |
| extra=extra) |
| |
| def ex_list_subnets(self): |
| """ |
| Get a list of Subnet that are available. |
| |
| :rtype: ``list`` of :class:`OpenStack_2_SubNet` |
| """ |
| response = self.network_connection.request( |
| self._subnets_url_prefix).object |
| return self._to_subnets(response) |
| |
| def ex_create_subnet(self, name, network, cidr, ip_version=4, |
| description='', dns_nameservers=None, |
| host_routes=None): |
| """ |
| Create a new Subnet |
| |
| :param name: Name of subnet which should be used |
| :type name: ``str`` |
| |
| :param network: Parent network of the subnet |
| :type network: ``OpenStackNetwork`` |
| |
| :param cidr: cidr of network which should be used |
| :type cidr: ``str`` |
| |
| :param ip_version: ip_version of subnet which should be used |
| :type ip_version: ``int`` |
| |
| :param description: Description for the resource. |
| :type description: ``str`` |
| |
| :param dns_nameservers: List of dns name servers. |
| :type dns_nameservers: ``list`` of ``str`` |
| |
| :param host_routes: Additional routes for the subnet. |
| :type host_routes: ``list`` of ``str`` |
| |
| :rtype: :class:`OpenStack_2_SubNet` |
| """ |
| data = { |
| 'subnet': |
| { |
| 'cidr': cidr, |
| 'network_id': network.id, |
| 'ip_version': ip_version, |
| 'name': name or '', |
| 'description': description or '', |
| 'dns_nameservers': dns_nameservers or [], |
| 'host_routes': host_routes or [] |
| } |
| } |
| response = self.network_connection.request( |
| self._subnets_url_prefix, method='POST', data=data).object |
| return self._to_subnet(response['subnet']) |
| |
| def ex_delete_subnet(self, subnet): |
| """ |
| Delete a Subnet |
| |
| :param subnet: Subnet which should be deleted |
| :type subnet: :class:`OpenStack_2_SubNet` |
| |
| :rtype: ``bool`` |
| """ |
| resp = self.network_connection.request('%s/%s' % ( |
| self._subnets_url_prefix, subnet.id), method='DELETE') |
| return resp.status in (httplib.NO_CONTENT, httplib.ACCEPTED) |
| |
| def ex_update_subnet(self, subnet, name=None, description=None, |
| dns_nameservers=None, host_routes=None): |
| """ |
| Update data of an existing SubNet |
| |
| :param subnet: Subnet which should be updated |
| :type subnet: :class:`OpenStack_2_SubNet` |
| |
| :param name: Name of subnet which should be used |
| :type name: ``str`` |
| |
| :param description: Description for the resource. |
| :type description: ``str`` |
| |
| :param dns_nameservers: List of dns name servers. |
| :type dns_nameservers: ``list`` of ``str`` |
| |
| :param host_routes: Additional routes for the subnet. |
| :type host_routes: ``list`` of ``str`` |
| |
| :rtype: :class:`OpenStack_2_SubNet` |
| """ |
| data = {'subnet': {}} |
| if name is not None: |
| data['subnet']['name'] = name |
| if description is not None: |
| data['subnet']['description'] = description |
| if dns_nameservers is not None: |
| data['subnet']['dns_nameservers'] = dns_nameservers |
| if host_routes is not None: |
| data['subnet']['host_routes'] = host_routes |
| response = self.network_connection.request( |
| "%s/%s" % (self._subnets_url_prefix, subnet.id), |
| method='PUT', data=data).object |
| return self._to_subnet(response['subnet']) |
| |
| def ex_list_ports(self): |
| """ |
| List all OpenStack_2_PortInterfaces |
| |
| https://developer.openstack.org/api-ref/network/v2/#list-ports |
| |
| :rtype: ``list`` of :class:`OpenStack_2_PortInterface` |
| """ |
| response = self._paginated_request( |
| '/v2.0/ports', 'ports', self.network_connection) |
| return [self._to_port(port) for port in response['ports']] |
| |
| def ex_delete_port(self, port): |
| """ |
| Delete an OpenStack_2_PortInterface |
| |
| https://developer.openstack.org/api-ref/network/v2/#delete-port |
| |
| :param port: port interface to remove |
| :type port: :class:`OpenStack_2_PortInterface` |
| |
| :rtype: ``bool`` |
| """ |
| response = self.network_connection.request( |
| '/v2.0/ports/%s' % port.id, method='DELETE' |
| ) |
| return response.success() |
| |
| def ex_detach_port_interface(self, node, port): |
| """ |
| Detaches an OpenStack_2_PortInterface interface from a Node. |
| :param node: node |
| :type node: :class:`Node` |
| |
| :param port: port interface to detach |
| :type port: :class:`OpenStack_2_PortInterface` |
| |
| :rtype: ``bool`` |
| """ |
| return self.connection.request( |
| '/servers/%s/os-interface/%s' % (node.id, port.id), |
| method='DELETE' |
| ).success() |
| |
| def ex_attach_port_interface(self, node, port): |
| """ |
| Attaches an OpenStack_2_PortInterface to a Node. |
| |
| :param node: node |
| :type node: :class:`Node` |
| |
| :param port: port interface to attach |
| :type port: :class:`OpenStack_2_PortInterface` |
| |
| :rtype: ``bool`` |
| """ |
| data = { |
| 'interfaceAttachment': { |
| 'port_id': port.id |
| } |
| } |
| return self.connection.request( |
| '/servers/{}/os-interface'.format(node.id), |
| method='POST', data=data |
| ).success() |
| |
| def ex_create_port(self, network, description=None, |
| admin_state_up=True, name=None): |
| """ |
| Creates a new OpenStack_2_PortInterface |
| |
| :param network: ID of the network where the newly created |
| port should be attached to |
| :type network: :class:`OpenStackNetwork` |
| |
| :param description: Description of the port |
| :type description: str |
| |
| :param admin_state_up: The administrative state of the |
| resource, which is up or down |
| :type admin_state_up: bool |
| |
| :param name: Human-readable name of the resource |
| :type name: str |
| |
| :rtype: :class:`OpenStack_2_PortInterface` |
| """ |
| data = { |
| 'port': |
| { |
| 'description': description or '', |
| 'admin_state_up': admin_state_up, |
| 'name': name or '', |
| 'network_id': network.id, |
| } |
| } |
| response = self.network_connection.request( |
| '/v2.0/ports', method='POST', data=data |
| ) |
| return self._to_port(response.object['port']) |
| |
| def ex_get_port(self, port_interface_id): |
| """ |
| Retrieve the OpenStack_2_PortInterface with the given ID |
| |
| :param port_interface_id: ID of the requested port |
| :type port_interface_id: str |
| |
| :return: :class:`OpenStack_2_PortInterface` |
| """ |
| response = self.network_connection.request( |
| '/v2.0/ports/{}'.format(port_interface_id), method='GET' |
| ) |
| return self._to_port(response.object['port']) |
| |
| def ex_update_port(self, port, description=None, |
| admin_state_up=None, name=None, |
| port_security_enabled=None, |
| qos_policy_id=None, security_groups=None): |
| """ |
| Update a OpenStack_2_PortInterface |
| |
| :param port: port interface to update |
| :type port: :class:`OpenStack_2_PortInterface` |
| |
| :param description: Description of the port |
| :type description: ``str`` |
| |
| :param admin_state_up: The administrative state of the |
| resource, which is up or down |
| :type admin_state_up: ``bool`` |
| |
| :param name: Human-readable name of the resource |
| :type name: ``str`` |
| |
| :param port_security_enabled: The port security status |
| :type port_security_enabled: ``bool`` |
| |
| :param qos_policy_id: QoS policy associated with the port |
| :type qos_policy_id: ``str`` |
| |
| :param security_groups: The IDs of security groups applied |
| :type security_groups: ``list`` of ``str`` |
| |
| :rtype: :class:`OpenStack_2_PortInterface` |
| """ |
| data = {'port': {}} |
| if description is not None: |
| data['port']['description'] = description |
| if admin_state_up is not None: |
| data['port']['admin_state_up'] = admin_state_up |
| if name is not None: |
| data['port']['name'] = name |
| if port_security_enabled is not None: |
| data['port']['port_security_enabled'] = port_security_enabled |
| if qos_policy_id is not None: |
| data['port']['qos_policy_id'] = qos_policy_id |
| if security_groups is not None: |
| data['port']['security_groups'] = security_groups |
| response = self.network_connection.request( |
| '/v2.0/ports/{}'.format(port.id), method='PUT', data=data |
| ) |
| return self._to_port(response.object['port']) |
| |
| def list_volumes(self): |
| """ |
| Get a list of Volumes that are available. |
| |
| :rtype: ``list`` of :class:`StorageVolume` |
| """ |
| return self._to_volumes(self._paginated_request( |
| '/volumes/detail', 'volumes', self.volumev2_connection)) |
| |
| def ex_get_volume(self, volumeId): |
| """ |
| Retrieve the StorageVolume with the given ID |
| |
| :param volumeId: ID of the volume |
| :type volumeId: ``string`` |
| |
| :return: :class:`StorageVolume` |
| """ |
| return self._to_volume( |
| self.volumev2_connection.request('/volumes/%s' % volumeId).object) |
| |
| def create_volume(self, size, name, location=None, snapshot=None, |
| ex_volume_type=None, ex_image_ref=None): |
| """ |
| Create a new volume. |
| |
| :param size: Size of volume in gigabytes (required) |
| :type size: ``int`` |
| |
| :param name: Name of the volume to be created |
| :type name: ``str`` |
| |
| :param location: Which data center to create a volume in. If |
| empty, undefined behavior will be selected. |
| (optional) |
| :type location: :class:`.NodeLocation` |
| |
| :param snapshot: Snapshot from which to create the new |
| volume. (optional) |
| :type snapshot: :class:`.VolumeSnapshot` |
| |
| :param ex_volume_type: What kind of volume to create. |
| (optional) |
| :type ex_volume_type: ``str`` |
| |
| :param ex_image_ref: The image to create the volume from |
| when creating a bootable volume (optional) |
| :type ex_image_ref: ``str`` |
| |
| :return: The newly created volume. |
| :rtype: :class:`StorageVolume` |
| """ |
| volume = { |
| 'name': name, |
| 'description': name, |
| 'size': size, |
| 'metadata': { |
| 'contents': name, |
| }, |
| } |
| |
| if ex_volume_type: |
| volume['volume_type'] = ex_volume_type |
| |
| if ex_image_ref: |
| volume['imageRef'] = ex_image_ref |
| |
| if location: |
| volume['availability_zone'] = location |
| |
| if snapshot: |
| volume['snapshot_id'] = snapshot.id |
| |
| resp = self.volumev2_connection.request('/volumes', |
| method='POST', |
| data={'volume': volume}) |
| return self._to_volume(resp.object) |
| |
| def destroy_volume(self, volume): |
| """ |
| Delete a Volume. |
| |
| :param volume: Volume to be deleted |
| :type volume: :class:`StorageVolume` |
| |
| :rtype: ``bool`` |
| """ |
| return self.volumev2_connection.request('/volumes/%s' % volume.id, |
| method='DELETE').success() |
| |
| def ex_list_snapshots(self): |
| """ |
| Get a list of Snapshot that are available. |
| |
| :rtype: ``list`` of :class:`VolumeSnapshot` |
| """ |
| return self._to_snapshots(self._paginated_request( |
| '/snapshots/detail', 'snapshots', self.volumev2_connection)) |
| |
| def create_volume_snapshot(self, volume, name=None, ex_description=None, |
| ex_force=True): |
| """ |
| Create snapshot from volume |
| |
| :param volume: Instance of `StorageVolume` |
| :type volume: `StorageVolume` |
| |
| :param name: Name of snapshot (optional) |
| :type name: `str` | `NoneType` |
| |
| :param ex_description: Description of the snapshot (optional) |
| :type ex_description: `str` | `NoneType` |
| |
| :param ex_force: Specifies if we create a snapshot that is not in |
| state `available`. For example `in-use`. Defaults |
| to True. (optional) |
| :type ex_force: `bool` |
| |
| :rtype: :class:`VolumeSnapshot` |
| """ |
| data = {'snapshot': {'volume_id': volume.id, 'force': ex_force}} |
| |
| if name is not None: |
| data['snapshot']['name'] = name |
| |
| if ex_description is not None: |
| data['snapshot']['description'] = ex_description |
| |
| return self._to_snapshot( |
| self.volumev2_connection.request('/snapshots', method='POST', |
| data=data).object) |
| |
| def destroy_volume_snapshot(self, snapshot): |
| """ |
| Delete a Volume Snapshot. |
| |
| :param snapshot: Snapshot to be deleted |
| :type snapshot: :class:`VolumeSnapshot` |
| |
| :rtype: ``bool`` |
| """ |
| resp = self.volumev2_connection.request('/snapshots/%s' % snapshot.id, |
| method='DELETE') |
| return resp.status in (httplib.NO_CONTENT, httplib.ACCEPTED) |
| |
| def ex_list_security_groups(self): |
| """ |
| Get a list of Security Groups that are available. |
| |
| :rtype: ``list`` of :class:`OpenStackSecurityGroup` |
| """ |
| return self._to_security_groups( |
| self.network_connection.request('/v2.0/security-groups').object) |
| |
| def ex_create_security_group(self, name, description): |
| """ |
| Create a new Security Group |
| |
| :param name: Name of the new Security Group |
| :type name: ``str`` |
| |
| :param description: Description of the new Security Group |
| :type description: ``str`` |
| |
| :rtype: :class:`OpenStackSecurityGroup` |
| """ |
| return self._to_security_group(self.network_connection .request( |
| '/v2.0/security-groups', method='POST', |
| data={'security_group': {'name': name, 'description': description}} |
| ).object['security_group']) |
| |
| def ex_delete_security_group(self, security_group): |
| """ |
| Delete a Security Group. |
| |
| :param security_group: Security Group should be deleted |
| :type security_group: :class:`OpenStackSecurityGroup` |
| |
| :rtype: ``bool`` |
| """ |
| resp = self.network_connection.request('/v2.0/security-groups/%s' % |
| (security_group.id), |
| method='DELETE') |
| return resp.status == httplib.NO_CONTENT |
| |
| def _to_security_group_rule(self, obj): |
| ip_range = group = tenant_id = parent_id = None |
| protocol = from_port = to_port = direction = None |
| |
| if 'parent_group_id' in obj: |
| if obj['group'] == {}: |
| ip_range = obj['ip_range'].get('cidr', None) |
| else: |
| group = obj['group'].get('name', None) |
| tenant_id = obj['group'].get('tenant_id', None) |
| |
| parent_id = obj['parent_group_id'] |
| from_port = obj['from_port'] |
| to_port = obj['to_port'] |
| protocol = obj['ip_protocol'] |
| else: |
| ip_range = obj.get('remote_ip_prefix', None) |
| group = obj.get('remote_group_id', None) |
| tenant_id = obj.get('tenant_id', None) |
| |
| parent_id = obj['security_group_id'] |
| from_port = obj['port_range_min'] |
| to_port = obj['port_range_max'] |
| protocol = obj['protocol'] |
| |
| return OpenStackSecurityGroupRule( |
| id=obj['id'], parent_group_id=parent_id, |
| ip_protocol=protocol, from_port=from_port, |
| to_port=to_port, driver=self, ip_range=ip_range, |
| group=group, tenant_id=tenant_id, direction=direction) |
| |
| def ex_create_security_group_rule(self, security_group, ip_protocol, |
| from_port, to_port, cidr=None, |
| source_security_group=None): |
| """ |
| Create a new Rule in a Security Group |
| |
| :param security_group: Security Group in which to add the rule |
| :type security_group: :class:`OpenStackSecurityGroup` |
| |
| :param ip_protocol: Protocol to which this rule applies |
| Examples: tcp, udp, ... |
| :type ip_protocol: ``str`` |
| |
| :param from_port: First port of the port range |
| :type from_port: ``int`` |
| |
| :param to_port: Last port of the port range |
| :type to_port: ``int`` |
| |
| :param cidr: CIDR notation of the source IP range for this rule |
| :type cidr: ``str`` |
| |
| :param source_security_group: Existing Security Group to use as the |
| source (instead of CIDR) |
| :type source_security_group: L{OpenStackSecurityGroup |
| |
| :rtype: :class:`OpenStackSecurityGroupRule` |
| """ |
| source_security_group_id = None |
| if type(source_security_group) == OpenStackSecurityGroup: |
| source_security_group_id = source_security_group.id |
| |
| return self._to_security_group_rule(self.network_connection.request( |
| '/v2.0/security-group-rules', method='POST', |
| data={'security_group_rule': { |
| 'direction': 'ingress', |
| 'protocol': ip_protocol, |
| 'port_range_min': from_port, |
| 'port_range_max': to_port, |
| 'remote_ip_prefix': cidr, |
| 'remote_group_id': source_security_group_id, |
| 'security_group_id': security_group.id}} |
| ).object['security_group_rule']) |
| |
| def ex_delete_security_group_rule(self, rule): |
| """ |
| Delete a Rule from a Security Group. |
| |
| :param rule: Rule should be deleted |
| :type rule: :class:`OpenStackSecurityGroupRule` |
| |
| :rtype: ``bool`` |
| """ |
| resp = self.network_connection.request( |
| '/v2.0/security-group-rules/%s' % (rule.id), method='DELETE') |
| return resp.status == httplib.NO_CONTENT |
| |
| def ex_remove_security_group_from_node(self, security_group, node): |
| """ |
| Remove a Security Group from a node. |
| |
| :param security_group: Security Group to remove from node. |
| :type security_group: :class:`OpenStackSecurityGroup` |
| |
| :param node: Node to remove the Security Group. |
| :type node: :class:`Node` |
| |
| :rtype: ``bool`` |
| """ |
| server_params = {'name': security_group.name} |
| resp = self._node_action(node, 'removeSecurityGroup', **server_params) |
| return resp.status == httplib.ACCEPTED |
| |
| def _to_floating_ip_pool(self, obj): |
| return OpenStack_2_FloatingIpPool(obj['id'], obj['name'], |
| self.network_connection) |
| |
| def _to_floating_ip_pools(self, obj): |
| pool_elements = obj['networks'] |
| return [self._to_floating_ip_pool(pool) for pool in pool_elements] |
| |
| def ex_list_floating_ip_pools(self): |
| """ |
| List available floating IP pools |
| |
| :rtype: ``list`` of :class:`OpenStack_2_FloatingIpPool` |
| """ |
| return self._to_floating_ip_pools( |
| self.network_connection.request('/v2.0/networks?router:external' |
| '=True&fields=id&fields=' |
| 'name').object) |
| |
| def _to_routers(self, obj): |
| routers = obj['routers'] |
| return [self._to_router(router) for router in routers] |
| |
| def _to_router(self, obj): |
| extra = {} |
| extra['external_gateway_info'] = obj['external_gateway_info'] |
| extra['routes'] = obj['routes'] |
| return OpenStack_2_Router(id=obj['id'], |
| name=obj['name'], |
| status=obj['status'], |
| driver=self, |
| extra=extra) |
| |
| def ex_list_routers(self): |
| """ |
| Get a list of Routers that are available. |
| |
| :rtype: ``list`` of :class:`OpenStack_2_Router` |
| """ |
| response = self.network_connection.request( |
| '/v2.0/routers').object |
| return self._to_routers(response) |
| |
| def ex_create_router(self, name, description='', admin_state_up=True, |
| external_gateway_info=None): |
| """ |
| Create a new Router |
| |
| :param name: Name of router which should be used |
| :type name: ``str`` |
| |
| :param description: Description of the port |
| :type description: ``str`` |
| |
| :param admin_state_up: The administrative state of the |
| resource, which is up or down |
| :type admin_state_up: ``bool`` |
| |
| :param external_gateway_info: The external gateway information |
| :type external_gateway_info: ``dict`` |
| |
| :rtype: :class:`OpenStack_2_Router` |
| """ |
| data = { |
| 'router': |
| { |
| 'name': name or '', |
| 'description': description or '', |
| 'admin_state_up': admin_state_up, |
| } |
| } |
| if external_gateway_info: |
| data['router']['external_gateway_info'] = external_gateway_info |
| response = self.network_connection.request( |
| '/v2.0/routers', method='POST', data=data).object |
| return self._to_router(response['router']) |
| |
| def ex_delete_router(self, router): |
| """ |
| Delete a Router |
| |
| :param router: Router which should be deleted |
| :type router: :class:`OpenStack_2_Router` |
| |
| :rtype: ``bool`` |
| """ |
| resp = self.network_connection.request('%s/%s' % ( |
| '/v2.0/routers', router.id), method='DELETE') |
| return resp.status in (httplib.NO_CONTENT, httplib.ACCEPTED) |
| |
| def _manage_router_interface(self, router, op, subnet=None, port=None): |
| """ |
| Add/Remove interface to router |
| |
| :param router: Router to add/remove the interface |
| :type router: :class:`OpenStack_2_Router` |
| |
| :param op: Operation to perform: 'add' or 'remove' |
| :type op: ``str`` |
| |
| :param subnet: Subnet object to be added to the router |
| :type subnet: :class:`OpenStack_2_SubNet` |
| |
| :param port: Port object to be added to the router |
| :type port: :class:`OpenStack_2_PortInterface` |
| |
| :rtype: ``bool`` |
| """ |
| data = {} |
| if subnet: |
| data['subnet_id'] = subnet.id |
| elif port: |
| data['port_id'] = port.id |
| else: |
| raise OpenStackException("Error in router interface: " |
| "port or subnet are None.", 500, |
| self) |
| |
| resp = self.network_connection.request('%s/%s/%s_router_interface' % ( |
| '/v2.0/routers', router.id, op), method='PUT', data=data) |
| return resp.status == httplib.OK |
| |
| def ex_add_router_port(self, router, port): |
| """ |
| Add port to a router |
| |
| :param router: Router to add the port |
| :type router: :class:`OpenStack_2_Router` |
| |
| :param port: Port object to be added to the router |
| :type port: :class:`OpenStack_2_PortInterface` |
| |
| :rtype: ``bool`` |
| """ |
| return self._manage_router_interface(router, 'add', port=port) |
| |
| def ex_del_router_port(self, router, port): |
| """ |
| Remove port from a router |
| |
| :param router: Router to remove the port |
| :type router: :class:`OpenStack_2_Router` |
| |
| :param port: Port object to be added to the router |
| :type port: :class:`OpenStack_2_PortInterface` |
| |
| :rtype: ``bool`` |
| """ |
| return self._manage_router_interface(router, 'remove', port=port) |
| |
| def ex_add_router_subnet(self, router, subnet): |
| """ |
| Add subnet to a router |
| |
| :param router: Router to add the subnet |
| :type router: :class:`OpenStack_2_Router` |
| |
| :param subnet: Subnet object to be added to the router |
| :type subnet: :class:`OpenStack_2_SubNet` |
| |
| :rtype: ``bool`` |
| """ |
| return self._manage_router_interface(router, 'add', subnet=subnet) |
| |
| def ex_del_router_subnet(self, router, subnet): |
| """ |
| Remove subnet to a router |
| |
| :param router: Router to remove the subnet |
| :type router: :class:`OpenStack_2_Router` |
| |
| :param subnet: Subnet object to be added to the router |
| :type subnet: :class:`OpenStack_2_SubNet` |
| |
| :rtype: ``bool`` |
| """ |
| return self._manage_router_interface(router, 'remove', subnet=subnet) |
| |
| def _to_quota_set(self, obj): |
| res = OpenStack_2_QuotaSet( |
| id=obj['id'], |
| cores=obj['cores'], |
| instances=obj['instances'], |
| key_pairs=obj['key_pairs'], |
| metadata_items=obj['metadata_items'], |
| ram=obj['ram'], |
| server_groups=obj['server_groups'], |
| server_group_members=obj['server_group_members'], |
| fixed_ips=obj.get('fixed_ips', None), |
| floating_ips=obj.get('floating_ips', None), |
| networks=obj.get('networks', None), |
| security_group_rules=obj.get('security_group_rules', None), |
| security_groups=obj.get('security_groups', None), |
| injected_file_content_bytes=obj.get('injected_file_content_bytes', |
| None), |
| injected_file_path_bytes=obj.get('injected_file_path_bytes', None), |
| injected_files=obj.get('injected_files', None), |
| driver=self.connection.driver) |
| |
| return res |
| |
| def ex_get_quota_set(self, tenant_id, user_id=None): |
| """ |
| Get the quota for a project or a project and a user. |
| |
| :param tenant_id: The UUID of the tenant in a multi-tenancy cloud |
| :type tenant_id: ``str`` |
| |
| :param user_id: ID of user to list the quotas for. |
| :type user_id: ``str`` |
| |
| :rtype: :class:`OpenStack_2_QuotaSet` |
| """ |
| url = '/os-quota-sets/%s/detail' % tenant_id |
| if user_id: |
| url += "?user_id=%s" % user_id |
| return self._to_quota_set( |
| self.connection.request(url).object['quota_set']) |
| |
| |
| class OpenStack_1_1_FloatingIpPool(object): |
| """ |
| Floating IP Pool info. |
| """ |
| |
| def __init__(self, name, connection): |
| self.name = name |
| self.connection = connection |
| |
| def list_floating_ips(self): |
| """ |
| List floating IPs in the pool |
| |
| :rtype: ``list`` of :class:`OpenStack_1_1_FloatingIpAddress` |
| """ |
| return self._to_floating_ips( |
| self.connection.request('/os-floating-ips').object) |
| |
| def _to_floating_ips(self, obj): |
| ip_elements = obj['floating_ips'] |
| return [self._to_floating_ip(ip) for ip in ip_elements] |
| |
| def _to_floating_ip(self, obj): |
| return OpenStack_1_1_FloatingIpAddress(id=obj['id'], |
| ip_address=obj['ip'], |
| pool=self, |
| node_id=obj['instance_id'], |
| driver=self.connection.driver) |
| |
| def get_floating_ip(self, ip): |
| """ |
| Get specified floating IP from the pool |
| |
| :param ip: floating IP to get |
| :type ip: ``str`` |
| |
| :rtype: :class:`OpenStack_1_1_FloatingIpAddress` |
| """ |
| ip_obj, = [x for x in self.list_floating_ips() if x.ip_address == ip] |
| return ip_obj |
| |
| def create_floating_ip(self): |
| """ |
| Create new floating IP in the pool |
| |
| :rtype: :class:`OpenStack_1_1_FloatingIpAddress` |
| """ |
| resp = self.connection.request('/os-floating-ips', |
| method='POST', |
| data={'pool': self.name}) |
| data = resp.object['floating_ip'] |
| id = data['id'] |
| ip_address = data['ip'] |
| return OpenStack_1_1_FloatingIpAddress(id=id, |
| ip_address=ip_address, |
| pool=self, |
| node_id=None, |
| driver=self.connection.driver) |
| |
| def delete_floating_ip(self, ip): |
| """ |
| Delete specified floating IP from the pool |
| |
| :param ip: floating IP to remove |
| :type ip: :class:`OpenStack_1_1_FloatingIpAddress` |
| |
| :rtype: ``bool`` |
| """ |
| resp = self.connection.request('/os-floating-ips/%s' % ip.id, |
| method='DELETE') |
| return resp.status in (httplib.NO_CONTENT, httplib.ACCEPTED) |
| |
| def __repr__(self): |
| return ('<OpenStack_1_1_FloatingIpPool: name=%s>' % self.name) |
| |
| |
| class OpenStack_1_1_FloatingIpAddress(object): |
| """ |
| Floating IP info. |
| """ |
| |
| def __init__(self, id, ip_address, pool, node_id=None, driver=None): |
| self.id = str(id) |
| self.ip_address = ip_address |
| self.pool = pool |
| self.node_id = node_id |
| self.driver = driver |
| |
| def delete(self): |
| """ |
| Delete this floating IP |
| |
| :rtype: ``bool`` |
| """ |
| if self.pool is not None: |
| return self.pool.delete_floating_ip(self) |
| elif self.driver is not None: |
| return self.driver.ex_delete_floating_ip(self) |
| |
| def __repr__(self): |
| return ('<OpenStack_1_1_FloatingIpAddress: id=%s, ip_addr=%s,' |
| ' pool=%s, driver=%s>' |
| % (self.id, self.ip_address, self.pool, self.driver)) |
| |
| |
| class OpenStack_2_FloatingIpPool(object): |
| """ |
| Floating IP Pool info. |
| """ |
| |
| def __init__(self, id, name, connection): |
| self.id = id |
| self.name = name |
| self.connection = connection |
| |
| def _to_floating_ips(self, obj): |
| ip_elements = obj['floatingips'] |
| return [self._to_floating_ip(ip) for ip in ip_elements] |
| |
| def _to_floating_ip(self, obj): |
| instance_id = None |
| |
| # In neutron version prior to 13.0.0 port_details does not exists |
| if 'port_details' not in obj and 'port_id' in obj and obj['port_id']: |
| port = self.connection.driver.ex_get_port(obj['port_id']) |
| if port: |
| obj['port_details'] = {"device_id": port.extra["device_id"], |
| "device_owner": |
| port.extra["device_owner"], |
| "mac_address": |
| port.extra["mac_address"]} |
| |
| if 'port_details' in obj and obj['port_details']: |
| dev_owner = obj['port_details']['device_owner'] |
| if dev_owner and dev_owner.startswith("compute:"): |
| instance_id = obj['port_details']['device_id'] |
| |
| ip_address = obj['floating_ip_address'] |
| return OpenStack_1_1_FloatingIpAddress(id=obj['id'], |
| ip_address=ip_address, |
| pool=self, |
| node_id=instance_id, |
| driver=self.connection.driver) |
| |
| def list_floating_ips(self): |
| """ |
| List floating IPs in the pool |
| |
| :rtype: ``list`` of :class:`OpenStack_1_1_FloatingIpAddress` |
| """ |
| return self._to_floating_ips( |
| self.connection.request('/v2.0/floatingips').object) |
| |
| def get_floating_ip(self, ip): |
| """ |
| Get specified floating IP from the pool |
| |
| :param ip: floating IP to get |
| :type ip: ``str`` |
| |
| :rtype: :class:`OpenStack_1_1_FloatingIpAddress` |
| """ |
| floating_ips = self._to_floating_ips( |
| self.connection.request('/v2.0/floatingips?floating_ip_address' |
| '=%s' % ip).object) |
| return floating_ips[0] |
| |
| def create_floating_ip(self): |
| """ |
| Create new floating IP in the pool |
| |
| :rtype: :class:`OpenStack_1_1_FloatingIpAddress` |
| """ |
| resp = self.connection.request('/v2.0/floatingips', |
| method='POST', |
| data={'floatingip': |
| {'floating_network_id': self.id}} |
| ) |
| data = resp.object['floatingip'] |
| id = data['id'] |
| ip_address = data['floating_ip_address'] |
| return OpenStack_1_1_FloatingIpAddress(id=id, |
| ip_address=ip_address, |
| pool=self, |
| node_id=None, |
| driver=self.connection.driver) |
| |
| def delete_floating_ip(self, ip): |
| """ |
| Delete specified floating IP from the pool |
| |
| :param ip: floating IP to remove |
| :type ip: :class:`OpenStack_1_1_FloatingIpAddress` |
| |
| :rtype: ``bool`` |
| """ |
| resp = self.connection.request('/v2.0/floatingips/%s' % ip.id, |
| method='DELETE') |
| return resp.status in (httplib.NO_CONTENT, httplib.ACCEPTED) |
| |
| def __repr__(self): |
| return ('<OpenStack_2_FloatingIpPool: name=%s>' % self.name) |
| |
| |
| class OpenStack_2_SubNet(object): |
| """ |
| A Virtual SubNet. |
| """ |
| |
| def __init__(self, id, name, cidr, network_id, driver, extra=None): |
| self.id = str(id) |
| self.name = name |
| self.cidr = cidr |
| self.network_id = network_id |
| self.driver = driver |
| self.extra = extra or {} |
| |
| def __repr__(self): |
| return '<OpenStack_2_SubNet id="%s" name="%s" cidr="%s">' % (self.id, |
| self.name, |
| self.cidr) |
| |
| |
| class OpenStack_2_Router(object): |
| """ |
| A Virtual Router. |
| """ |
| |
| def __init__(self, id, name, status, driver, extra=None): |
| self.id = str(id) |
| self.name = name |
| self.status = status |
| self.driver = driver |
| self.extra = extra or {} |
| |
| def __repr__(self): |
| return '<OpenStack_2_Router id="%s" name="%s">' % (self.id, |
| self.name) |
| |
| |
| class OpenStack_2_PortInterface(UuidMixin): |
| """ |
| Port Interface info. Similar in functionality to a floating IP (can be |
| attached / detached from a compute instance) but implementation-wise a |
| bit different. |
| |
| > A port is a connection point for attaching a single device, such as the |
| > NIC of a server, to a network. The port also describes the associated |
| > network configuration, such as the MAC and IP addresses to be used on |
| > that port. |
| https://docs.openstack.org/python-openstackclient/pike/cli/command-objects/port.html |
| |
| Also see: |
| https://developer.openstack.org/api-ref/compute/#port-interfaces-servers-os-interface |
| """ |
| |
| def __init__(self, id, state, driver, created=None, extra=None): |
| """ |
| :param id: Port Interface ID. |
| :type id: ``str`` |
| :param state: State of the OpenStack_2_PortInterface. |
| :type state: :class:`.OpenStack_2_PortInterfaceState` |
| :param created: A datetime object that represents when the |
| port interface was created |
| :type created: ``datetime.datetime`` |
| :param extra: Optional provided specific attributes associated with |
| this image. |
| :type extra: ``dict`` |
| """ |
| self.id = str(id) |
| self.state = state |
| self.driver = driver |
| self.created = created |
| self.extra = extra or {} |
| UuidMixin.__init__(self) |
| |
| def delete(self): |
| """ |
| Delete this Port Interface |
| |
| :rtype: ``bool`` |
| """ |
| return self.driver.ex_delete_port(self) |
| |
| def __repr__(self): |
| return (('<OpenStack_2_PortInterface: id=%s, state=%s, ' |
| 'driver=%s ...>') |
| % (self.id, self.state, self.driver.name)) |
| |
| |
| class OpenStack_2_QuotaSetItem(object): |
| """ |
| Qouta Set Item info. Each item has three attributes: in_use, |
| limit and reserved. |
| |
| See: |
| https://docs.openstack.org/api-ref/compute/?expanded=show-the-detail-of-quota-detail#show-a-quota |
| """ |
| |
| def __init__(self, in_use, limit, reserved): |
| """ |
| :param in_use: Number of currently used resources. |
| :type in_use: ``int`` |
| :param limit: Max number of available resources. |
| :type limit: ``int`` |
| :param reserved: Number of reserved resources. |
| :type reserved: ``int`` |
| """ |
| self.in_use = in_use |
| self.limit = limit |
| self.reserved = reserved |
| |
| def __repr__(self): |
| return ('<OpenStack_2_QuotaSetItem in_use="%s", limit="%s",' |
| 'reserved="%s">' % (self.in_use, self.limit, |
| self.reserved)) |
| |
| |
| class OpenStack_2_QuotaSet(object): |
| """ |
| Qouta Set info. To get the informatio about quotas and used resources. |
| |
| See: |
| https://docs.openstack.org/api-ref/compute/?expanded=show-the-detail-of-quota-detail#show-a-quota |
| |
| """ |
| |
| def __init__(self, id, cores, instances, key_pairs, metadata_items, ram, |
| server_groups, server_group_members, fixed_ips=None, |
| floating_ips=None, networks=None, security_group_rules=None, |
| security_groups=None, injected_file_content_bytes=None, |
| injected_file_path_bytes=None, injected_files=None, |
| driver=None): |
| """ |
| :param id: Quota Set ID. |
| :type id: ``str`` |
| :param cores: Quota Set of cores. |
| :type cores: :class:`.OpenStack_2_QuotaSetItem` or ``dict`` |
| :param instances: Quota Set of instances. |
| :type instances: :class:`.OpenStack_2_QuotaSetItem` or ``dict`` |
| :param key_pairs: Quota Set of key pairs. |
| :type key_pairs: :class:`.OpenStack_2_QuotaSetItem` or ``dict`` |
| :param metadata_items: Quota Set of metadata items. |
| :type metadata_items: :class:`.OpenStack_2_QuotaSetItem` or ``dict`` |
| :param ram: Quota Set of RAM. |
| :type ram: :class:`.OpenStack_2_QuotaSetItem` or ``dict`` |
| :param server_groups: Quota Set of server groups. |
| :type server_groups: :class:`.OpenStack_2_QuotaSetItem` or ``dict`` |
| :param fixed_ips: Quota Set of fixed ips. (optional) |
| :type fixed_ips: :class:`.OpenStack_2_QuotaSetItem` or ``dict`` |
| :param floating_ips: Quota Set of floating ips. (optional) |
| :type floating_ips: :class:`.OpenStack_2_QuotaSetItem` or ``dict`` |
| :param networks: Quota Set of networks. (optional) |
| :type networks: :class:`.OpenStack_2_QuotaSetItem` or ``dict`` |
| :param security_group_rules: Quota Set of security group rules. |
| (optional) |
| :type security_group_rules: :class:`.OpenStack_2_QuotaSetItem` |
| or ``dict`` |
| :param security_groups: Quota Set of security groups. (optional) |
| :type security_groups: :class:`.OpenStack_2_QuotaSetItem` or ``dict`` |
| :param injected_file_content_bytes: Quota Set of injected file content |
| bytes. (optional) |
| :type injected_file_content_bytes: :class:`.OpenStack_2_QuotaSetItem` |
| or ``dict`` |
| :param injected_file_path_bytes: Quota Set of injected file path bytes. |
| (optional) |
| :type injected_file_path_bytes: :class:`.OpenStack_2_QuotaSetItem` |
| or ``dict`` |
| :param injected_files: Quota Set of injected files. (optional) |
| :type injected_files: :class:`.OpenStack_2_QuotaSetItem` or ``dict`` |
| """ |
| self.id = str(id) |
| self.cores = self._to_quota_set_item(cores) |
| self.instances = self._to_quota_set_item(instances) |
| self.key_pairs = self._to_quota_set_item(key_pairs) |
| self.metadata_items = self._to_quota_set_item(metadata_items) |
| self.ram = self._to_quota_set_item(ram) |
| self.server_groups = self._to_quota_set_item(server_groups) |
| self.server_group_members = self._to_quota_set_item( |
| server_group_members) |
| self.fixed_ips = self._to_quota_set_item(fixed_ips) |
| self.floating_ips = self._to_quota_set_item(floating_ips) |
| self.networks = self._to_quota_set_item(networks) |
| self.security_group_rules = self._to_quota_set_item( |
| security_group_rules) |
| self.security_groups = self._to_quota_set_item(security_groups) |
| self.injected_file_content_bytes = self._to_quota_set_item( |
| injected_file_content_bytes) |
| self.injected_file_path_bytes = self._to_quota_set_item( |
| injected_file_path_bytes) |
| self.injected_files = self._to_quota_set_item(injected_files) |
| self.driver = driver |
| |
| def _to_quota_set_item(self, obj): |
| if obj: |
| if isinstance(obj, OpenStack_2_QuotaSetItem): |
| return obj |
| elif isinstance(obj, dict): |
| return OpenStack_2_QuotaSetItem(obj['in_use'], obj['limit'], |
| obj['reserved']) |
| else: |
| return None |
| |
| def __repr__(self): |
| return ('<OpenStack_2_QuotaSet id="%s", cores="%s", ram="%s",' |
| ' instances="%s">' % (self.id, self.cores, self.ram, |
| self.instances)) |