blob: a268ad052a0315c92c183724bfa54b6825f2b955 [file] [log] [blame]
# 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.
"""
Module for Google Compute Engine Driver.
"""
from __future__ import with_statement
import datetime
import time
import sys
from libcloud.common.google import GoogleResponse
from libcloud.common.google import GoogleBaseConnection
from libcloud.common.google import GoogleBaseError
from libcloud.common.google import ResourceNotFoundError
from libcloud.common.google import ResourceExistsError
from libcloud.compute.base import Node, NodeDriver, NodeImage, NodeLocation
from libcloud.compute.base import NodeSize, StorageVolume, VolumeSnapshot
from libcloud.compute.base import UuidMixin
from libcloud.compute.providers import Provider
from libcloud.compute.types import NodeState
API_VERSION = 'v1'
DEFAULT_TASK_COMPLETION_TIMEOUT = 180
def timestamp_to_datetime(timestamp):
"""
Return a datetime object that corresponds to the time in an RFC3339
timestamp.
:param timestamp: RFC3339 timestamp string
:type timestamp: ``str``
:return: Datetime object corresponding to timestamp
:rtype: :class:`datetime.datetime`
"""
# We remove timezone offset and microseconds (Python 2.5 strptime doesn't
# support %f)
ts = datetime.datetime.strptime(timestamp[:-10], '%Y-%m-%dT%H:%M:%S')
tz_hours = int(timestamp[-5:-3])
tz_mins = int(timestamp[-2:]) * int(timestamp[-6:-5] + '1')
tz_delta = datetime.timedelta(hours=tz_hours, minutes=tz_mins)
return ts + tz_delta
class GCEResponse(GoogleResponse):
pass
class GCEConnection(GoogleBaseConnection):
"""Connection class for the GCE driver."""
host = 'www.googleapis.com'
responseCls = GCEResponse
def __init__(self, user_id, key, secure, auth_type=None,
credential_file=None, project=None, **kwargs):
super(GCEConnection, self).__init__(user_id, key, secure=secure,
auth_type=auth_type,
credential_file=credential_file,
**kwargs)
self.request_path = '/compute/%s/projects/%s' % (API_VERSION,
project)
class GCEAddress(UuidMixin):
"""A GCE Static address."""
def __init__(self, id, name, address, region, driver, extra=None):
self.id = str(id)
self.name = name
self.address = address
self.region = region
self.driver = driver
self.extra = extra
UuidMixin.__init__(self)
def destroy(self):
"""
Destroy this address.
:return: True if successful
:rtype: ``bool``
"""
return self.driver.ex_destroy_address(address=self)
def __repr__(self):
return '<GCEAddress id="%s" name="%s" address="%s">' % (
self.id, self.name, self.address)
class GCEFailedDisk(object):
"""Dummy Node object for disks that are not created."""
def __init__(self, name, error, code):
self.name = name
self.error = error
self.code = code
def __repr__(self):
return '<GCEFailedDisk name="%s" error_code="%s">' % (
self.name, self.code)
class GCEFailedNode(object):
"""Dummy Node object for nodes that are not created."""
def __init__(self, name, error, code):
self.name = name
self.error = error
self.code = code
def __repr__(self):
return '<GCEFailedNode name="%s" error_code="%s">' % (
self.name, self.code)
class GCEHealthCheck(UuidMixin):
"""A GCE Http Health Check class."""
def __init__(self, id, name, path, port, interval, timeout,
unhealthy_threshold, healthy_threshold, driver, extra=None):
self.id = str(id)
self.name = name
self.path = path
self.port = port
self.interval = interval
self.timeout = timeout
self.unhealthy_threshold = unhealthy_threshold
self.healthy_threshold = healthy_threshold
self.driver = driver
self.extra = extra
UuidMixin.__init__(self)
def destroy(self):
"""
Destroy this Health Check.
:return: True if successful
:rtype: ``bool``
"""
return self.driver.ex_destroy_healthcheck(healthcheck=self)
def update(self):
"""
Commit updated healthcheck values.
:return: Updated Healthcheck object
:rtype: :class:`GCEHealthcheck`
"""
return self.driver.ex_update_healthcheck(healthcheck=self)
def __repr__(self):
return '<GCEHealthCheck id="%s" name="%s" path="%s" port="%s">' % (
self.id, self.name, self.path, self.port)
class GCEFirewall(UuidMixin):
"""A GCE Firewall rule class."""
def __init__(self, id, name, allowed, network, source_ranges, source_tags,
target_tags, driver, extra=None):
self.id = str(id)
self.name = name
self.network = network
self.allowed = allowed
self.source_ranges = source_ranges
self.source_tags = source_tags
self.target_tags = target_tags
self.driver = driver
self.extra = extra
UuidMixin.__init__(self)
def destroy(self):
"""
Destroy this firewall.
:return: True if successful
:rtype: ``bool``
"""
return self.driver.ex_destroy_firewall(firewall=self)
def update(self):
"""
Commit updated firewall values.
:return: Updated Firewall object
:rtype: :class:`GCEFirewall`
"""
return self.driver.ex_update_firewall(firewall=self)
def __repr__(self):
return '<GCEFirewall id="%s" name="%s" network="%s">' % (
self.id, self.name, self.network.name)
class GCEForwardingRule(UuidMixin):
def __init__(self, id, name, region, address, protocol, targetpool, driver,
extra=None):
self.id = str(id)
self.name = name
self.region = region
self.address = address
self.protocol = protocol
self.targetpool = targetpool
self.driver = driver
self.extra = extra
UuidMixin.__init__(self)
def destroy(self):
"""
Destroy this Forwarding Rule
:return: True if successful
:rtype: ``bool``
"""
return self.driver.ex_destroy_forwarding_rule(forwarding_rule=self)
def __repr__(self):
return '<GCEForwardingRule id="%s" name="%s" address="%s">' % (
self.id, self.name, self.address)
class GCENodeImage(NodeImage):
"""A GCE Node Image class."""
def __init__(self, id, name, driver, extra=None):
super(GCENodeImage, self).__init__(id, name, driver, extra=extra)
def delete(self):
"""
Delete this image
:return: True if successful
:rtype: ``bool``
"""
return self.driver.ex_delete_image(image=self)
def deprecate(self, replacement, state):
"""
Deprecate this image
:param replacement: Image to use as a replacement
:type replacement: ``str`` or :class: `GCENodeImage`
:param state: Deprecation state of this image. Possible values include
\'DELETED\', \'DEPRECATED\' or \'OBSOLETE\'.
:type state: ``str``
:return: True if successful
:rtype: ``bool``
"""
return self.driver.ex_deprecate_image(self, replacement, state)
class GCENetwork(UuidMixin):
"""A GCE Network object class."""
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
UuidMixin.__init__(self)
def destroy(self):
"""
Destroy this network
:return: True if successful
:rtype: ``bool``
"""
return self.driver.ex_destroy_network(network=self)
def __repr__(self):
return '<GCENetwork id="%s" name="%s" cidr="%s">' % (
self.id, self.name, self.cidr)
class GCENodeSize(NodeSize):
"""A GCE Node Size (MachineType) class."""
def __init__(self, id, name, ram, disk, bandwidth, price, driver,
extra=None):
self.extra = extra
super(GCENodeSize, self).__init__(id, name, ram, disk, bandwidth,
price, driver, extra=extra)
class GCEProject(UuidMixin):
"""GCE Project information."""
def __init__(self, id, name, metadata, quotas, driver, extra=None):
self.id = str(id)
self.name = name
self.metadata = metadata
self.quotas = quotas
self.driver = driver
self.extra = extra
UuidMixin.__init__(self)
def __repr__(self):
return '<GCEProject id="%s" name="%s">' % (self.id, self.name)
class GCERegion(UuidMixin):
def __init__(self, id, name, status, zones, quotas, deprecated, driver,
extra=None):
self.id = str(id)
self.name = name
self.status = status
self.zones = zones
self.quotas = quotas
self.deprecated = deprecated
self.driver = driver
self.extra = extra
UuidMixin.__init__(self)
def __repr__(self):
return '<GCERegion id="%s" name="%s", status="%s">' % (
self.id, self.name, self.status)
class GCESnapshot(VolumeSnapshot):
def __init__(self, id, name, size, status, driver, extra=None):
self.name = name
self.status = status
super(GCESnapshot, self).__init__(id, driver, size, extra)
class GCETargetPool(UuidMixin):
def __init__(self, id, name, region, healthchecks, nodes, driver,
extra=None):
self.id = str(id)
self.name = name
self.region = region
self.healthchecks = healthchecks
self.nodes = nodes
self.driver = driver
self.extra = extra
UuidMixin.__init__(self)
def add_node(self, node):
"""
Add a node to this target pool.
:param node: Node to add
:type node: ``str`` or :class:`Node`
:return: True if successful
:rtype: ``bool``
"""
return self.driver.ex_targetpool_add_node(targetpool=self, node=node)
def remove_node(self, node):
"""
Remove a node from this target pool.
:param node: Node to remove
:type node: ``str`` or :class:`Node`
:return: True if successful
:rtype: ``bool``
"""
return self.driver.ex_targetpool_remove_node(targetpool=self,
node=node)
def add_healthcheck(self, healthcheck):
"""
Add a healthcheck to this target pool.
:param healthcheck: Healthcheck to add
:type healthcheck: ``str`` or :class:`GCEHealthCheck`
:return: True if successful
:rtype: ``bool``
"""
return self.driver.ex_targetpool_add_healthcheck(
targetpool=self, healthcheck=healthcheck)
def remove_healthcheck(self, healthcheck):
"""
Remove a healthcheck from this target pool.
:param healthcheck: Healthcheck to remove
:type healthcheck: ``str`` or :class:`GCEHealthCheck`
:return: True if successful
:rtype: ``bool``
"""
return self.driver.ex_targetpool_remove_healthcheck(
targetpool=self, healthcheck=healthcheck)
def destroy(self):
"""
Destroy this Target Pool
:return: True if successful
:rtype: ``bool``
"""
return self.driver.ex_destroy_targetpool(targetpool=self)
def __repr__(self):
return '<GCETargetPool id="%s" name="%s" region="%s">' % (
self.id, self.name, self.region.name)
class GCEZone(NodeLocation):
"""Subclass of NodeLocation to provide additional information."""
def __init__(self, id, name, status, maintenance_windows, deprecated,
driver, extra=None):
self.status = status
self.maintenance_windows = maintenance_windows
self.deprecated = deprecated
self.extra = extra
country = name.split('-')[0]
super(GCEZone, self).__init__(id=str(id), name=name, country=country,
driver=driver)
@property
def time_until_mw(self):
"""
Returns the time until the next Maintenance Window as a
datetime.timedelta object.
"""
return self._get_time_until_mw()
@property
def next_mw_duration(self):
"""
Returns the duration of the next Maintenance Window as a
datetime.timedelta object.
"""
return self._get_next_mw_duration()
def _now(self):
"""
Returns current UTC time.
Can be overridden in unittests.
"""
return datetime.datetime.utcnow()
def _get_next_maint(self):
"""
Returns the next Maintenance Window.
:return: A dictionary containing maintenance window info (or None if
no maintenance windows are scheduled)
The dictionary contains 4 keys with values of type ``str``
- name: The name of the maintenance window
- description: Description of the maintenance window
- beginTime: RFC3339 Timestamp
- endTime: RFC3339 Timestamp
:rtype: ``dict`` or ``None``
"""
begin = None
next_window = None
if not self.maintenance_windows:
return None
if len(self.maintenance_windows) == 1:
return self.maintenance_windows[0]
for mw in self.maintenance_windows:
begin_next = timestamp_to_datetime(mw['beginTime'])
if (not begin) or (begin_next < begin):
begin = begin_next
next_window = mw
return next_window
def _get_time_until_mw(self):
"""
Returns time until next maintenance window.
:return: Time until next maintenance window (or None if no
maintenance windows are scheduled)
:rtype: :class:`datetime.timedelta` or ``None``
"""
next_window = self._get_next_maint()
if not next_window:
return None
now = self._now()
next_begin = timestamp_to_datetime(next_window['beginTime'])
return next_begin - now
def _get_next_mw_duration(self):
"""
Returns the duration of the next maintenance window.
:return: Duration of next maintenance window (or None if no
maintenance windows are scheduled)
:rtype: :class:`datetime.timedelta` or ``None``
"""
next_window = self._get_next_maint()
if not next_window:
return None
next_begin = timestamp_to_datetime(next_window['beginTime'])
next_end = timestamp_to_datetime(next_window['endTime'])
return next_end - next_begin
def __repr__(self):
return '<GCEZone id="%s" name="%s" status="%s">' % (self.id, self.name,
self.status)
class GCENodeDriver(NodeDriver):
"""
GCE Node Driver class.
This is the primary driver for interacting with Google Compute Engine. It
contains all of the standard libcloud methods, plus additional ex_* methods
for more features.
Note that many methods allow either objects or strings (or lists of
objects/strings). In most cases, passing strings instead of objects will
result in additional GCE API calls.
"""
connectionCls = GCEConnection
api_name = 'googleapis'
name = "Google Compute Engine"
type = Provider.GCE
website = 'https://cloud.google.com/'
NODE_STATE_MAP = {
"PROVISIONING": NodeState.PENDING,
"STAGING": NodeState.PENDING,
"RUNNING": NodeState.RUNNING,
"STOPPED": NodeState.TERMINATED,
"STOPPING": NodeState.TERMINATED,
"TERMINATED": NodeState.TERMINATED
}
AUTH_URL = "https://www.googleapis.com/auth/"
SA_SCOPES_MAP = {
# list derived from 'gcloud compute instances create --help'
"bigquery": "bigquery",
"compute-ro": "compute.readonly",
"compute-rw": "compute",
"datastore": "datastore",
"sql": "sqlservice",
"sql-admin": "sqlservice.admin",
"storage-full": "devstorage.full_control",
"storage-ro": "devstorage.read_only",
"storage-rw": "devstorage.read_write",
"taskqueue": "taskqueue",
"userinfo-email": "userinfo.email"
}
def __init__(self, user_id, key=None, datacenter=None, project=None,
auth_type=None, scopes=None, credential_file=None, **kwargs):
"""
:param user_id: The email address (for service accounts) or Client ID
(for installed apps) to be used for authentication.
:type user_id: ``str``
:param key: The RSA Key (for service accounts) or file path containing
key or Client Secret (for installed apps) to be used for
authentication.
:type key: ``str``
:keyword datacenter: The name of the datacenter (zone) used for
operations.
:type datacenter: ``str``
:keyword project: Your GCE project name. (required)
:type project: ``str``
:keyword auth_type: Accepted values are "SA" or "IA" or "GCE"
("Service Account" or "Installed Application" or
"GCE" if libcloud is being used on a GCE instance
with service account enabled).
If not supplied, auth_type will be guessed based
on value of user_id or if the code is being
executed in a GCE instance.
:type auth_type: ``str``
:keyword scopes: List of authorization URLs. Default is empty and
grants read/write to Compute, Storage, DNS.
:type scopes: ``list``
:keyword credential_file: Path to file for caching authentication
information used by GCEConnection.
:type credential_file: ``str``
"""
if not project:
raise ValueError('Project name must be specified using '
'"project" keyword.')
self.auth_type = auth_type
self.project = project
self.scopes = scopes
self.credential_file = credential_file or \
'~/.gce_libcloud_auth' + '.' + self.project
super(GCENodeDriver, self).__init__(user_id, key, **kwargs)
# Cache Zone and Region information to reduce API calls and
# increase speed
self.base_path = '/compute/%s/projects/%s' % (API_VERSION,
self.project)
self.zone_list = self.ex_list_zones()
self.zone_dict = {}
for zone in self.zone_list:
self.zone_dict[zone.name] = zone
if datacenter:
self.zone = self.ex_get_zone(datacenter)
else:
self.zone = None
self.region_list = self.ex_list_regions()
self.region_dict = {}
for region in self.region_list:
self.region_dict[region.name] = region
if self.zone:
self.region = self._get_region_from_zone(self.zone)
else:
self.region = None
def ex_list_addresses(self, region=None):
"""
Return a list of static addresses for a region or all.
:keyword region: The region to return addresses from. For example:
'us-central1'. If None, will return addresses from
region of self.zone. If 'all', will return all
addresses.
:type region: ``str`` or ``None``
:return: A list of static address objects.
:rtype: ``list`` of :class:`GCEAddress`
"""
list_addresses = []
region = self._set_region(region)
if region is None:
request = '/aggregated/addresses'
else:
request = '/regions/%s/addresses' % (region.name)
response = self.connection.request(request, method='GET').object
if 'items' in response:
# The aggregated result returns dictionaries for each region
if region is None:
for v in response['items'].values():
region_addresses = [self._to_address(a) for a in
v.get('addresses', [])]
list_addresses.extend(region_addresses)
else:
list_addresses = [self._to_address(a) for a in
response['items']]
return list_addresses
def ex_list_healthchecks(self):
"""
Return the list of health checks.
:return: A list of health check objects.
:rtype: ``list`` of :class:`GCEHealthCheck`
"""
list_healthchecks = []
request = '/global/httpHealthChecks'
response = self.connection.request(request, method='GET').object
list_healthchecks = [self._to_healthcheck(h) for h in
response.get('items', [])]
return list_healthchecks
def ex_list_firewalls(self):
"""
Return the list of firewalls.
:return: A list of firewall objects.
:rtype: ``list`` of :class:`GCEFirewall`
"""
list_firewalls = []
request = '/global/firewalls'
response = self.connection.request(request, method='GET').object
list_firewalls = [self._to_firewall(f) for f in
response.get('items', [])]
return list_firewalls
def ex_list_forwarding_rules(self, region=None):
"""
Return the list of forwarding rules for a region or all.
:keyword region: The region to return forwarding rules from. For
example: 'us-central1'. If None, will return
forwarding rules from the region of self.region
(which is based on self.zone). If 'all', will
return all forwarding rules.
:type region: ``str`` or :class:`GCERegion` or ``None``
:return: A list of forwarding rule objects.
:rtype: ``list`` of :class:`GCEForwardingRule`
"""
list_forwarding_rules = []
region = self._set_region(region)
if region is None:
request = '/aggregated/forwardingRules'
else:
request = '/regions/%s/forwardingRules' % (region.name)
response = self.connection.request(request, method='GET').object
if 'items' in response:
# The aggregated result returns dictionaries for each region
if region is None:
for v in response['items'].values():
region_forwarding_rules = [self._to_forwarding_rule(f) for
f in v.get('forwardingRules',
[])]
list_forwarding_rules.extend(region_forwarding_rules)
else:
list_forwarding_rules = [self._to_forwarding_rule(f) for f in
response['items']]
return list_forwarding_rules
def list_images(self, ex_project=None):
"""
Return a list of image objects for a project.
:keyword ex_project: Optional alternate project name.
:type ex_project: ``str`` or ``None``
:return: List of GCENodeImage objects
:rtype: ``list`` of :class:`GCENodeImage`
"""
request = '/global/images'
if ex_project is None:
response = self.connection.request(request, method='GET').object
else:
# Save the connection request_path
save_request_path = self.connection.request_path
# Override the connection request path
new_request_path = save_request_path.replace(self.project,
ex_project)
self.connection.request_path = new_request_path
response = self.connection.request(request, method='GET').object
# Restore the connection request_path
self.connection.request_path = save_request_path
list_images = [self._to_node_image(i) for i in
response.get('items', [])]
return list_images
def list_locations(self):
"""
Return a list of locations (zones).
The :class:`ex_list_zones` method returns more comprehensive results,
but this is here for compatibility.
:return: List of NodeLocation objects
:rtype: ``list`` of :class:`NodeLocation`
"""
list_locations = []
request = '/zones'
response = self.connection.request(request, method='GET').object
list_locations = [self._to_node_location(l) for l in response['items']]
return list_locations
def ex_list_networks(self):
"""
Return the list of networks.
:return: A list of network objects.
:rtype: ``list`` of :class:`GCENetwork`
"""
list_networks = []
request = '/global/networks'
response = self.connection.request(request, method='GET').object
list_networks = [self._to_network(n) for n in
response.get('items', [])]
return list_networks
def list_nodes(self, ex_zone=None):
"""
Return a list of nodes in the current zone or all zones.
:keyword ex_zone: Optional zone name or 'all'
:type ex_zone: ``str`` or :class:`GCEZone` or
:class:`NodeLocation` or ``None``
:return: List of Node objects
:rtype: ``list`` of :class:`Node`
"""
list_nodes = []
zone = self._set_zone(ex_zone)
if zone is None:
request = '/aggregated/instances'
else:
request = '/zones/%s/instances' % (zone.name)
response = self.connection.request(request, method='GET').object
if 'items' in response:
# The aggregated response returns a dict for each zone
if zone is None:
for v in response['items'].values():
zone_nodes = [self._to_node(i) for i in
v.get('instances', [])]
list_nodes.extend(zone_nodes)
else:
list_nodes = [self._to_node(i) for i in response['items']]
return list_nodes
def ex_list_regions(self):
"""
Return the list of regions.
:return: A list of region objects.
:rtype: ``list`` of :class:`GCERegion`
"""
list_regions = []
request = '/regions'
response = self.connection.request(request, method='GET').object
list_regions = [self._to_region(r) for r in response['items']]
return list_regions
def list_sizes(self, location=None):
"""
Return a list of sizes (machineTypes) in a zone.
:keyword location: Location or Zone for sizes
:type location: ``str`` or :class:`GCEZone` or
:class:`NodeLocation` or ``None``
:return: List of GCENodeSize objects
:rtype: ``list`` of :class:`GCENodeSize`
"""
list_sizes = []
zone = self._set_zone(location)
if zone is None:
request = '/aggregated/machineTypes'
else:
request = '/zones/%s/machineTypes' % (zone.name)
response = self.connection.request(request, method='GET').object
if 'items' in response:
# The aggregated response returns a dict for each zone
if zone is None:
for v in response['items'].values():
zone_sizes = [self._to_node_size(s) for s in
v.get('machineTypes', [])]
list_sizes.extend(zone_sizes)
else:
list_sizes = [self._to_node_size(s) for s in response['items']]
return list_sizes
def ex_list_snapshots(self):
"""
Return the list of disk snapshots in the project.
:return: A list of snapshot objects
:rtype: ``list`` of :class:`GCESnapshot`
"""
list_snapshots = []
request = '/global/snapshots'
response = self.connection.request(request, method='GET').object
list_snapshots = [self._to_snapshot(s) for s in
response.get('items', [])]
return list_snapshots
def ex_list_targetpools(self, region=None):
"""
Return the list of target pools.
:return: A list of target pool objects
:rtype: ``list`` of :class:`GCETargetPool`
"""
list_targetpools = []
region = self._set_region(region)
if region is None:
request = '/aggregated/targetPools'
else:
request = '/regions/%s/targetPools' % (region.name)
response = self.connection.request(request, method='GET').object
if 'items' in response:
# The aggregated result returns dictionaries for each region
if region is None:
for v in response['items'].values():
region_targetpools = [self._to_targetpool(t) for t in
v.get('targetPools', [])]
list_targetpools.extend(region_targetpools)
else:
list_targetpools = [self._to_targetpool(t) for t in
response['items']]
return list_targetpools
def list_volumes(self, ex_zone=None):
"""
Return a list of volumes for a zone or all.
Will return list from provided zone, or from the default zone unless
given the value of 'all'.
:keyword ex_zone: The zone to return volumes from.
:type ex_zone: ``str`` or :class:`GCEZone` or
:class:`NodeLocation` or ``None``
:return: A list of volume objects.
:rtype: ``list`` of :class:`StorageVolume`
"""
list_volumes = []
zone = self._set_zone(ex_zone)
if zone is None:
request = '/aggregated/disks'
else:
request = '/zones/%s/disks' % (zone.name)
response = self.connection.request(request, method='GET').object
if 'items' in response:
# The aggregated response returns a dict for each zone
if zone is None:
for v in response['items'].values():
zone_volumes = [self._to_storage_volume(d) for d in
v.get('disks', [])]
list_volumes.extend(zone_volumes)
else:
list_volumes = [self._to_storage_volume(d) for d in
response['items']]
return list_volumes
def ex_list_zones(self):
"""
Return the list of zones.
:return: A list of zone objects.
:rtype: ``list`` of :class:`GCEZone`
"""
list_zones = []
request = '/zones'
response = self.connection.request(request, method='GET').object
list_zones = [self._to_zone(z) for z in response['items']]
return list_zones
def ex_create_address(self, name, region=None, address=None):
"""
Create a static address in a region.
:param name: Name of static address
:type name: ``str``
:keyword region: Name of region for the address (e.g. 'us-central1')
:type region: ``str`` or :class:`GCERegion`
:keyword address: Ephemeral IP address to promote to a static one
(e.g. 'xxx.xxx.xxx.xxx')
:type address: ``str`` or ``None``
:return: Static Address object
:rtype: :class:`GCEAddress`
"""
region = region or self.region
if not hasattr(region, 'name'):
region = self.ex_get_region(region)
elif region is None:
raise ValueError('REGION_NOT_SPECIFIED',
'Region must be provided for an address')
address_data = {'name': name}
if address:
address_data['address'] = address
request = '/regions/%s/addresses' % (region.name)
self.connection.async_request(request, method='POST',
data=address_data)
return self.ex_get_address(name, region=region)
def ex_create_healthcheck(self, name, host=None, path=None, port=None,
interval=None, timeout=None,
unhealthy_threshold=None,
healthy_threshold=None,
description=None):
"""
Create an Http Health Check.
:param name: Name of health check
:type name: ``str``
:keyword host: Hostname of health check request. Defaults to empty
and public IP is used instead.
:type host: ``str``
:keyword path: The request path for the check. Defaults to /.
:type path: ``str``
:keyword port: The TCP port number for the check. Defaults to 80.
:type port: ``int``
:keyword interval: How often (in seconds) to check. Defaults to 5.
:type interval: ``int``
:keyword timeout: How long to wait before failing. Defaults to 5.
:type timeout: ``int``
:keyword unhealthy_threshold: How many failures before marking
unhealthy. Defaults to 2.
:type unhealthy_threshold: ``int``
:keyword healthy_threshold: How many successes before marking as
healthy. Defaults to 2.
:type healthy_threshold: ``int``
:keyword description: The description of the check. Defaults to None.
:type description: ``str`` or ``None``
:return: Health Check object
:rtype: :class:`GCEHealthCheck`
"""
hc_data = {}
hc_data['name'] = name
if host:
hc_data['host'] = host
if description:
hc_data['description'] = description
# As of right now, the 'default' values aren't getting set when called
# through the API, so set them explicitly
hc_data['requestPath'] = path or '/'
hc_data['port'] = port or 80
hc_data['checkIntervalSec'] = interval or 5
hc_data['timeoutSec'] = timeout or 5
hc_data['unhealthyThreshold'] = unhealthy_threshold or 2
hc_data['healthyThreshold'] = healthy_threshold or 2
request = '/global/httpHealthChecks'
self.connection.async_request(request, method='POST', data=hc_data)
return self.ex_get_healthcheck(name)
def ex_create_firewall(self, name, allowed, network='default',
source_ranges=None, source_tags=None,
target_tags=None):
"""
Create a firewall on a network.
Firewall rules should be supplied in the "allowed" field. This is a
list of dictionaries formated like so ("ports" is optional)::
[{"IPProtocol": "<protocol string or number>",
"ports": "<port_numbers or ranges>"}]
For example, to allow tcp on port 8080 and udp on all ports, 'allowed'
would be::
[{"IPProtocol": "tcp",
"ports": ["8080"]},
{"IPProtocol": "udp"}]
See `Firewall Reference <https://developers.google.com/compute/docs/
reference/latest/firewalls/insert>`_ for more information.
:param name: Name of the firewall to be created
:type name: ``str``
:param allowed: List of dictionaries with rules
:type allowed: ``list`` of ``dict``
:keyword network: The network that the firewall applies to.
:type network: ``str`` or :class:`GCENetwork`
:keyword source_ranges: A list of IP ranges in CIDR format that the
firewall should apply to. Defaults to
['0.0.0.0/0']
:type source_ranges: ``list`` of ``str``
:keyword source_tags: A list of source instance tags the rules apply
to.
:type source_tags: ``list`` of ``str``
:keyword target_tags: A list of target instance tags the rules apply
to.
:type target_tags: ``list`` of ``str``
:return: Firewall object
:rtype: :class:`GCEFirewall`
"""
firewall_data = {}
if not hasattr(network, 'name'):
nw = self.ex_get_network(network)
else:
nw = network
firewall_data['name'] = name
firewall_data['allowed'] = allowed
firewall_data['network'] = nw.extra['selfLink']
if source_ranges is None:
source_ranges = ['0.0.0.0/0']
firewall_data['sourceRanges'] = source_ranges
if source_tags is not None:
firewall_data['sourceTags'] = source_tags
if target_tags is not None:
firewall_data['targetTags'] = target_tags
request = '/global/firewalls'
self.connection.async_request(request, method='POST',
data=firewall_data)
return self.ex_get_firewall(name)
def ex_create_forwarding_rule(self, name, targetpool, region=None,
protocol='tcp', port_range=None,
address=None, description=None):
"""
Create a forwarding rule.
:param name: Name of forwarding rule to be created
:type name: ``str``
:param targetpool: Target pool to apply the rule to
:param targetpool: ``str`` or :class:`GCETargetPool`
:keyword region: Region to create the forwarding rule in. Defaults to
self.region
:type region: ``str`` or :class:`GCERegion`
:keyword protocol: Should be 'tcp' or 'udp'
:type protocol: ``str``
:keyword port_range: Optional single port number or range separated
by a dash. Examples: '80', '5000-5999'.
:type port_range: ``str``
:keyword address: Optional static address for forwarding rule. Must be
in same region.
:type address: ``str`` or :class:`GCEAddress`
:keyword description: The description of the forwarding rule.
Defaults to None.
:type description: ``str`` or ``None``
:return: Forwarding Rule object
:rtype: :class:`GCEForwardingRule`
"""
forwarding_rule_data = {}
region = region or self.region
if not hasattr(region, 'name'):
region = self.ex_get_region(region)
if not hasattr(targetpool, 'name'):
targetpool = self.ex_get_targetpool(targetpool, region)
forwarding_rule_data['name'] = name
forwarding_rule_data['region'] = region.extra['selfLink']
forwarding_rule_data['target'] = targetpool.extra['selfLink']
forwarding_rule_data['IPProtocol'] = protocol.upper()
if address:
if not hasattr(address, 'name'):
address = self.ex_get_address(address, region)
forwarding_rule_data['IPAddress'] = address.address
if port_range:
forwarding_rule_data['portRange'] = port_range
if description:
forwarding_rule_data['description'] = description
request = '/regions/%s/forwardingRules' % (region.name)
self.connection.async_request(request, method='POST',
data=forwarding_rule_data)
return self.ex_get_forwarding_rule(name)
def ex_create_image(self, name, volume, description=None,
use_existing=True, wait_for_completion=True):
"""
Create an image from the provided volume.
:param name: The name of the image to create.
:type name: ``str``
:param volume: The volume to use to create the image, or the
Google Cloud Storage URI
:type volume: ``str`` or :class:`StorageVolume`
:keyword description: Description of the new Image
:type description: ``str``
:keyword use_existing: If True and an image with the given name
already exists, return an object for that
image instead of attempting to create
a new image.
:type use_existing: ``bool``
:keyword wait_for_completion: If True, wait until the new image is
created before returning a new NodeImage
Otherwise, return a new NodeImage
instance, and let the user track the
creation progress
:type wait_for_completion: ``bool``
:return: A GCENodeImage object for the new image
:rtype: :class:`GCENodeImage`
"""
image_data = {}
image_data['name'] = name
image_data['description'] = description
if isinstance(volume, StorageVolume):
image_data['sourceDisk'] = volume.extra['selfLink']
image_data['zone'] = volume.extra['zone'].name
elif isinstance(volume, str) and \
volume.startswith('https://') and volume.endswith('tar.gz'):
image_data['rawDisk'] = {'source': volume, 'containerType': 'TAR'}
else:
raise ValueError('Source must be instance of StorageVolume or URI')
request = '/global/images'
try:
if wait_for_completion:
self.connection.async_request(request, method='POST',
data=image_data)
else:
self.connection.request(request, method='POST',
data=image_data)
except ResourceExistsError:
e = sys.exc_info()[1]
if not use_existing:
raise e
return self.ex_get_image(name)
def ex_create_network(self, name, cidr):
"""
Create a network.
:param name: Name of network to be created
:type name: ``str``
:param cidr: Address range of network in CIDR format.
:type cidr: ``str``
:return: Network object
:rtype: :class:`GCENetwork`
"""
network_data = {}
network_data['name'] = name
network_data['IPv4Range'] = cidr
request = '/global/networks'
self.connection.async_request(request, method='POST',
data=network_data)
return self.ex_get_network(name)
def create_node(self, name, size, image, location=None,
ex_network='default', ex_tags=None, ex_metadata=None,
ex_boot_disk=None, use_existing_disk=True,
external_ip='ephemeral', ex_disk_type='pd-standard',
ex_disk_auto_delete=True, ex_service_accounts=None):
"""
Create a new node and return a node object for the node.
:param name: The name of the node to create.
:type name: ``str``
:param size: The machine type to use.
:type size: ``str`` or :class:`GCENodeSize`
:param image: The image to use to create the node (or, if attaching
a persistent disk, the image used to create the disk)
:type image: ``str`` or :class:`GCENodeImage`
:keyword location: The location (zone) to create the node in.
:type location: ``str`` or :class:`NodeLocation` or
:class:`GCEZone` or ``None``
:keyword ex_network: The network to associate with the node.
:type ex_network: ``str`` or :class:`GCENetwork`
:keyword ex_tags: A list of tags to associate with the node.
:type ex_tags: ``list`` of ``str`` or ``None``
:keyword ex_metadata: Metadata dictionary for instance.
:type ex_metadata: ``dict`` or ``None``
:keyword ex_boot_disk: The boot disk to attach to the instance.
:type ex_boot_disk: :class:`StorageVolume` or ``str``
:keyword use_existing_disk: If True and if an existing disk with the
same name/location is found, use that
disk instead of creating a new one.
:type use_existing_disk: ``bool``
:keyword external_ip: The external IP address to use. If 'ephemeral'
(default), a new non-static address will be
used. If 'None', then no external address will
be used. To use an existing static IP address,
a GCEAddress object should be passed in.
:type external_ip: :class:`GCEAddress` or ``str`` or None
:keyword ex_disk_type: Specify a pd-standard (default) disk or pd-ssd
for an SSD disk.
:type ex_disk_type: ``str``
:keyword ex_disk_auto_delete: Indicate that the boot disk should be
deleted when the Node is deleted. Set to
True by default.
:type ex_disk_auto_delete: ``bool``
:keyword ex_service_accounts: Specify a list of serviceAccounts when
creating the instance. The format is a
list of dictionaries containing email
and list of scopes, e.g.
[{'email':'default',
'scopes':['compute', ...]}, ...]
Scopes can either be full URLs or short
names. If not provided, use the
'default' service account email and a
scope of 'devstorage.read_only'. Also
accepts the aliases defined in
'gcloud compute'.
:type ex_service_accounts: ``list``
:return: A Node object for the new node.
:rtype: :class:`Node`
"""
location = location or self.zone
if not hasattr(location, 'name'):
location = self.ex_get_zone(location)
if not hasattr(size, 'name'):
size = self.ex_get_size(size, location)
if not hasattr(ex_network, 'name'):
ex_network = self.ex_get_network(ex_network)
if not hasattr(image, 'name'):
image = self.ex_get_image(image)
if not ex_boot_disk:
ex_boot_disk = self.create_volume(None, name, location=location,
image=image,
use_existing=use_existing_disk,
ex_disk_type=ex_disk_type)
if not ex_metadata:
ex_metadata = None
elif not isinstance(ex_metadata, dict):
raise ValueError('metadata field is not a dictionnary.')
else:
if 'items' not in ex_metadata:
# The expected GCE format is odd:
# items: [{'value': '1', 'key': 'one'},
# {'value': '2', 'key': 'two'},
# {'value': 'N', 'key': 'N'}]
# So the only real key is items, and the values are tuples
# Since arbitrary values are fine, we only check for the key.
# If missing, we prefix it to the items.
items = []
for k, v in ex_metadata.items():
items.append({'key': k, 'value': v})
ex_metadata = {'items': items}
request, node_data = self._create_node_req(name, size, image,
location, ex_network,
ex_tags, ex_metadata,
ex_boot_disk, external_ip,
ex_disk_type,
ex_disk_auto_delete,
ex_service_accounts)
self.connection.async_request(request, method='POST', data=node_data)
return self.ex_get_node(name, location.name)
def ex_create_multiple_nodes(self, base_name, size, image, number,
location=None, ex_network='default',
ex_tags=None, ex_metadata=None,
ignore_errors=True, use_existing_disk=True,
poll_interval=2, external_ip='ephemeral',
ex_disk_type='pd-standard',
ex_auto_disk_delete=True,
ex_service_accounts=None,
timeout=DEFAULT_TASK_COMPLETION_TIMEOUT):
"""
Create multiple nodes and return a list of Node objects.
Nodes will be named with the base name and a number. For example, if
the base name is 'libcloud' and you create 3 nodes, they will be
named::
libcloud-000
libcloud-001
libcloud-002
:param base_name: The base name of the nodes to create.
:type base_name: ``str``
:param size: The machine type to use.
:type size: ``str`` or :class:`GCENodeSize`
:param image: The image to use to create the nodes.
:type image: ``str`` or :class:`GCENodeImage`
:param number: The number of nodes to create.
:type number: ``int``
:keyword location: The location (zone) to create the nodes in.
:type location: ``str`` or :class:`NodeLocation` or
:class:`GCEZone` or ``None``
:keyword ex_network: The network to associate with the nodes.
:type ex_network: ``str`` or :class:`GCENetwork`
:keyword ex_tags: A list of tags to assiciate with the nodes.
:type ex_tags: ``list`` of ``str`` or ``None``
:keyword ex_metadata: Metadata dictionary for instances.
:type ex_metadata: ``dict`` or ``None``
:keyword ignore_errors: If True, don't raise Exceptions if one or
more nodes fails.
:type ignore_errors: ``bool``
:keyword use_existing_disk: If True and if an existing disk with the
same name/location is found, use that
disk instead of creating a new one.
:type use_existing_disk: ``bool``
:keyword poll_interval: Number of seconds between status checks.
:type poll_interval: ``int``
:keyword external_ip: The external IP address to use. If 'ephemeral'
(default), a new non-static address will be
used. If 'None', then no external address will
be used. (Static addresses are not supported for
multiple node creation.)
:type external_ip: ``str`` or None
:keyword ex_disk_type: Specify a pd-standard (default) disk or pd-ssd
for an SSD disk.
:type ex_disk_type: ``str``
:keyword ex_disk_auto_delete: Indicate that the boot disk should be
deleted when the Node is deleted. Set to
True by default.
:type ex_disk_auto_delete: ``bool``
:keyword ex_service_accounts: Specify a list of serviceAccounts when
creating the instance. The format is a
list of dictionaries containing email
and list of scopes, e.g.
[{'email':'default',
'scopes':['compute', ...]}, ...]
Scopes can either be full URLs or short
names. If not provided, use the
'default' service account email and a
scope of 'devstorage.read_only'. Also
accepts the aliases defined in
'gcloud compute'.
:type ex_service_accounts: ``list``
:keyword timeout: The number of seconds to wait for all nodes to be
created before timing out.
:type timeout: ``int``
:return: A list of Node objects for the new nodes.
:rtype: ``list`` of :class:`Node`
"""
location = location or self.zone
if not hasattr(location, 'name'):
location = self.ex_get_zone(location)
if not hasattr(size, 'name'):
size = self.ex_get_size(size, location)
if not hasattr(ex_network, 'name'):
ex_network = self.ex_get_network(ex_network)
if not hasattr(image, 'name'):
image = self.ex_get_image(image)
node_attrs = {'size': size,
'image': image,
'location': location,
'network': ex_network,
'tags': ex_tags,
'metadata': ex_metadata,
'ignore_errors': ignore_errors,
'use_existing_disk': use_existing_disk,
'external_ip': external_ip,
'ex_disk_type': ex_disk_type,
'ex_service_accounts': ex_service_accounts}
# List for holding the status information for disk/node creation.
status_list = []
for i in range(number):
name = '%s-%03d' % (base_name, i)
status = {'name': name,
'node_response': None,
'node': None,
'disk_response': None,
'disk': None}
status_list.append(status)
# Create disks for nodes
for status in status_list:
self._multi_create_disk(status, node_attrs)
start_time = time.time()
complete = False
while not complete:
if (time.time() - start_time >= timeout):
raise Exception("Timeout (%s sec) while waiting for multiple "
"instances")
complete = True
time.sleep(poll_interval)
for status in status_list:
# If disk does not yet exist, check on its status
if not status['disk']:
self._multi_check_disk(status, node_attrs)
# If disk exists, but node does not, create the node or check
# on its status if already in progress.
if status['disk'] and not status['node']:
if not status['node_response']:
self._multi_create_node(status, node_attrs)
else:
self._multi_check_node(status, node_attrs)
# If any of the nodes have not been created (or failed) we are
# not done yet.
if not status['node']:
complete = False
# Return list of nodes
node_list = []
for status in status_list:
node_list.append(status['node'])
return node_list
def ex_create_targetpool(self, name, region=None, healthchecks=None,
nodes=None, session_affinity=None):
"""
Create a target pool.
:param name: Name of target pool
:type name: ``str``
:keyword region: Region to create the target pool in. Defaults to
self.region
:type region: ``str`` or :class:`GCERegion` or ``None``
:keyword healthchecks: Optional list of health checks to attach
:type healthchecks: ``list`` of ``str`` or :class:`GCEHealthCheck`
:keyword nodes: Optional list of nodes to attach to the pool
:type nodes: ``list`` of ``str`` or :class:`Node`
:keyword session_affinity: Optional algorithm to use for session
affinity.
:type session_affinity: ``str``
:return: Target Pool object
:rtype: :class:`GCETargetPool`
"""
region = region or self.region
targetpool_data = {}
targetpool_data['name'] = name
if not hasattr(region, 'name'):
region = self.ex_get_region(region)
targetpool_data['region'] = region.extra['selfLink']
if healthchecks:
if not hasattr(healthchecks[0], 'name'):
hc_list = [self.ex_get_healthcheck(h).extra['selfLink'] for h
in healthchecks]
else:
hc_list = [h.extra['selfLink'] for h in healthchecks]
targetpool_data['healthChecks'] = hc_list
if nodes:
if not hasattr(nodes[0], 'name'):
node_list = [self.ex_get_node(n, 'all').extra['selfLink'] for n
in nodes]
else:
node_list = [n.extra['selfLink'] for n in nodes]
targetpool_data['instances'] = node_list
if session_affinity:
targetpool_data['sessionAffinity'] = session_affinity
request = '/regions/%s/targetPools' % (region.name)
self.connection.async_request(request, method='POST',
data=targetpool_data)
return self.ex_get_targetpool(name, region)
def create_volume(self, size, name, location=None, snapshot=None,
image=None, use_existing=True,
ex_disk_type='pd-standard'):
"""
Create a volume (disk).
:param size: Size of volume to create (in GB). Can be None if image
or snapshot is supplied.
:type size: ``int`` or ``str`` or ``None``
:param name: Name of volume to create
:type name: ``str``
:keyword location: Location (zone) to create the volume in
:type location: ``str`` or :class:`GCEZone` or
:class:`NodeLocation` or ``None``
:keyword snapshot: Snapshot to create image from
:type snapshot: :class:`GCESnapshot` or ``str`` or ``None``
:keyword image: Image to create disk from.
:type image: :class:`GCENodeImage` or ``str`` or ``None``
:keyword use_existing: If True and a disk with the given name already
exists, return an object for that disk instead
of attempting to create a new disk.
:type use_existing: ``bool``
:keyword ex_disk_type: Specify a pd-standard (default) disk or pd-ssd
for an SSD disk.
:type ex_disk_type: ``str``
:return: Storage Volume object
:rtype: :class:`StorageVolume`
"""
request, volume_data, params = self._create_vol_req(
size, name, location, snapshot, image, ex_disk_type)
try:
self.connection.async_request(request, method='POST',
data=volume_data, params=params)
except ResourceExistsError:
e = sys.exc_info()[1]
if not use_existing:
raise e
return self.ex_get_volume(name, location)
def create_volume_snapshot(self, volume, name):
"""
Create a snapshot of the provided Volume.
:param volume: A StorageVolume object
:type volume: :class:`StorageVolume`
:return: A GCE Snapshot object
:rtype: :class:`GCESnapshot`
"""
snapshot_data = {}
snapshot_data['name'] = name
request = '/zones/%s/disks/%s/createSnapshot' % (
volume.extra['zone'].name, volume.name)
self.connection.async_request(request, method='POST',
data=snapshot_data)
return self.ex_get_snapshot(name)
def list_volume_snapshots(self, volume):
"""
List snapshots created from the provided volume.
For GCE, snapshots are global, but while the volume they were
created from still exists, the source disk for the snapshot is
tracked.
:param volume: A StorageVolume object
:type volume: :class:`StorageVolume`
:return: A list of Snapshot objects
:rtype: ``list`` of :class:`GCESnapshot`
"""
volume_snapshots = []
volume_link = volume.extra['selfLink']
all_snapshots = self.ex_list_snapshots()
for snapshot in all_snapshots:
if snapshot.extra['sourceDisk'] == volume_link:
volume_snapshots.append(snapshot)
return volume_snapshots
def ex_update_healthcheck(self, healthcheck):
"""
Update a health check with new values.
To update, change the attributes of the health check object and pass
the updated object to the method.
:param healthcheck: A healthcheck object with updated values.
:type healthcheck: :class:`GCEHealthCheck`
:return: An object representing the new state of the health check.
:rtype: :class:`GCEHealthCheck`
"""
hc_data = {}
hc_data['name'] = healthcheck.name
hc_data['requestPath'] = healthcheck.path
hc_data['port'] = healthcheck.port
hc_data['checkIntervalSec'] = healthcheck.interval
hc_data['timeoutSec'] = healthcheck.timeout
hc_data['unhealthyThreshold'] = healthcheck.unhealthy_threshold
hc_data['healthyThreshold'] = healthcheck.healthy_threshold
if healthcheck.extra['host']:
hc_data['host'] = healthcheck.extra['host']
if healthcheck.extra['description']:
hc_data['description'] = healthcheck.extra['description']
request = '/global/httpHealthChecks/%s' % (healthcheck.name)
self.connection.async_request(request, method='PUT',
data=hc_data)
return self.ex_get_healthcheck(healthcheck.name)
def ex_update_firewall(self, firewall):
"""
Update a firewall with new values.
To update, change the attributes of the firewall object and pass the
updated object to the method.
:param firewall: A firewall object with updated values.
:type firewall: :class:`GCEFirewall`
:return: An object representing the new state of the firewall.
:rtype: :class:`GCEFirewall`
"""
firewall_data = {}
firewall_data['name'] = firewall.name
firewall_data['allowed'] = firewall.allowed
firewall_data['network'] = firewall.network.extra['selfLink']
if firewall.source_ranges:
firewall_data['sourceRanges'] = firewall.source_ranges
if firewall.source_tags:
firewall_data['sourceTags'] = firewall.source_tags
if firewall.target_tags:
firewall_data['targetTags'] = firewall.target_tags
if firewall.extra['description']:
firewall_data['description'] = firewall.extra['description']
request = '/global/firewalls/%s' % (firewall.name)
self.connection.async_request(request, method='PUT',
data=firewall_data)
return self.ex_get_firewall(firewall.name)
def ex_targetpool_add_node(self, targetpool, node):
"""
Add a node to a target pool.
:param targetpool: The targetpool to add node to
:type targetpool: ``str`` or :class:`GCETargetPool`
:param node: The node to add
:type node: ``str`` or :class:`Node`
:returns: True if successful
:rtype: ``bool``
"""
if not hasattr(targetpool, 'name'):
targetpool = self.ex_get_targetpool(targetpool)
if hasattr(node, 'name'):
node_uri = node.extra['selfLink']
else:
if node.startswith('https://'):
node_uri = node
else:
node = self.ex_get_node(node, 'all')
node_uri = node.extra['selfLink']
targetpool_data = {'instances': [{'instance': node_uri}]}
request = '/regions/%s/targetPools/%s/addInstance' % (
targetpool.region.name, targetpool.name)
self.connection.async_request(request, method='POST',
data=targetpool_data)
if all((node_uri != n) and
(not hasattr(n, 'extra') or n.extra['selfLink'] != node_uri)
for n in targetpool.nodes):
targetpool.nodes.append(node)
return True
def ex_targetpool_add_healthcheck(self, targetpool, healthcheck):
"""
Add a health check to a target pool.
:param targetpool: The targetpool to add health check to
:type targetpool: ``str`` or :class:`GCETargetPool`
:param healthcheck: The healthcheck to add
:type healthcheck: ``str`` or :class:`GCEHealthCheck`
:returns: True if successful
:rtype: ``bool``
"""
if not hasattr(targetpool, 'name'):
targetpool = self.ex_get_targetpool(targetpool)
if not hasattr(healthcheck, 'name'):
healthcheck = self.ex_get_healthcheck(healthcheck)
targetpool_data = {
'healthChecks': [{'healthCheck': healthcheck.extra['selfLink']}]
}
request = '/regions/%s/targetPools/%s/addHealthCheck' % (
targetpool.region.name, targetpool.name)
self.connection.async_request(request, method='POST',
data=targetpool_data)
targetpool.healthchecks.append(healthcheck)
return True
def ex_targetpool_remove_node(self, targetpool, node):
"""
Remove a node from a target pool.
:param targetpool: The targetpool to remove node from
:type targetpool: ``str`` or :class:`GCETargetPool`
:param node: The node to remove
:type node: ``str`` or :class:`Node`
:returns: True if successful
:rtype: ``bool``
"""
if not hasattr(targetpool, 'name'):
targetpool = self.ex_get_targetpool(targetpool)
if hasattr(node, 'name'):
node_uri = node.extra['selfLink']
else:
if node.startswith('https://'):
node_uri = node
else:
node = self.ex_get_node(node, 'all')
node_uri = node.extra['selfLink']
targetpool_data = {'instances': [{'instance': node_uri}]}
request = '/regions/%s/targetPools/%s/removeInstance' % (
targetpool.region.name, targetpool.name)
self.connection.async_request(request, method='POST',
data=targetpool_data)
# Remove node object from node list
index = None
for i, nd in enumerate(targetpool.nodes):
if nd == node_uri or (hasattr(nd, 'extra') and
nd.extra['selfLink'] == node_uri):
index = i
break
if index is not None:
targetpool.nodes.pop(index)
return True
def ex_targetpool_remove_healthcheck(self, targetpool, healthcheck):
"""
Remove a health check from a target pool.
:param targetpool: The targetpool to remove health check from
:type targetpool: ``str`` or :class:`GCETargetPool`
:param healthcheck: The healthcheck to remove
:type healthcheck: ``str`` or :class:`GCEHealthCheck`
:returns: True if successful
:rtype: ``bool``
"""
if not hasattr(targetpool, 'name'):
targetpool = self.ex_get_targetpool(targetpool)
if not hasattr(healthcheck, 'name'):
healthcheck = self.ex_get_healthcheck(healthcheck)
targetpool_data = {
'healthChecks': [{'healthCheck': healthcheck.extra['selfLink']}]
}
request = '/regions/%s/targetPools/%s/removeHealthCheck' % (
targetpool.region.name, targetpool.name)
self.connection.async_request(request, method='POST',
data=targetpool_data)
# Remove healthcheck object from healthchecks list
index = None
for i, hc in enumerate(targetpool.healthchecks):
if hc.name == healthcheck.name:
index = i
if index is not None:
targetpool.healthchecks.pop(index)
return True
def reboot_node(self, node):
"""
Reboot a node.
:param node: Node to be rebooted
:type node: :class:`Node`
:return: True if successful, False if not
:rtype: ``bool``
"""
request = '/zones/%s/instances/%s/reset' % (node.extra['zone'].name,
node.name)
self.connection.async_request(request, method='POST',
data='ignored')
return True
def ex_set_node_tags(self, node, tags):
"""
Set the tags on a Node instance.
Note that this updates the node object directly.
:param node: Node object
:type node: :class:`Node`
:param tags: List of tags to apply to the object
:type tags: ``list`` of ``str``
:return: True if successful
:rtype: ``bool``
"""
request = '/zones/%s/instances/%s/setTags' % (node.extra['zone'].name,
node.name)
tags_data = {}
tags_data['items'] = tags
tags_data['fingerprint'] = node.extra['tags_fingerprint']
self.connection.async_request(request, method='POST',
data=tags_data)
new_node = self.ex_get_node(node.name, node.extra['zone'])
node.extra['tags'] = new_node.extra['tags']
node.extra['tags_fingerprint'] = new_node.extra['tags_fingerprint']
return True
def ex_set_node_scheduling(self, node, on_host_maintenance=None,
automatic_restart=None):
"""Set the maintenance behavior for the node.
See `Scheduling <https://developers.google.com/compute/
docs/instances#onhostmaintenance>`_ documentation for more info.
:param node: Node object
:type node: :class:`Node`
:keyword on_host_maintenance: Defines whether node should be
terminated or migrated when host machine
goes down. Acceptable values are:
'MIGRATE' or 'TERMINATE' (If not
supplied, value will be reset to GCE
default value for the instance type.)
:type on_host_maintenance: ``str``
:keyword automatic_restart: Defines whether the instance should be
automatically restarted when it is
terminated by Compute Engine. (If not
supplied, value will be set to the GCE
default value for the instance type.)
:type automatic_restart: ``bool``
:return: True if successful.
:rtype: ``bool``
"""
if not hasattr(node, 'name'):
node = self.ex_get_node(node, 'all')
if on_host_maintenance is not None:
on_host_maintenance = on_host_maintenance.upper()
ohm_values = ['MIGRATE', 'TERMINATE']
if on_host_maintenance not in ohm_values:
raise ValueError('on_host_maintenance must be one of %s' %
','.join(ohm_values))
request = '/zones/%s/instances/%s/setScheduling' % (
node.extra['zone'].name, node.name)
scheduling_data = {}
if on_host_maintenance is not None:
scheduling_data['onHostMaintenance'] = on_host_maintenance
if automatic_restart is not None:
scheduling_data['automaticRestart'] = automatic_restart
self.connection.async_request(request, method='POST',
data=scheduling_data)
new_node = self.ex_get_node(node.name, node.extra['zone'])
node.extra['scheduling'] = new_node.extra['scheduling']
ohm = node.extra['scheduling'].get('onHostMaintenance')
ar = node.extra['scheduling'].get('automaticRestart')
success = True
if on_host_maintenance not in [None, ohm]:
success = False
if automatic_restart not in [None, ar]:
success = False
return success
def deploy_node(self, name, size, image, script, location=None,
ex_network='default', ex_tags=None,
ex_service_accounts=None):
"""
Create a new node and run a script on start-up.
:param name: The name of the node to create.
:type name: ``str``
:param size: The machine type to use.
:type size: ``str`` or :class:`GCENodeSize`
:param image: The image to use to create the node.
:type image: ``str`` or :class:`GCENodeImage`
:param script: File path to start-up script
:type script: ``str``
:keyword location: The location (zone) to create the node in.
:type location: ``str`` or :class:`NodeLocation` or
:class:`GCEZone` or ``None``
:keyword ex_network: The network to associate with the node.
:type ex_network: ``str`` or :class:`GCENetwork`
:keyword ex_tags: A list of tags to associate with the node.
:type ex_tags: ``list`` of ``str`` or ``None``
:keyword ex_service_accounts: Specify a list of serviceAccounts when
creating the instance. The format is a
list of dictionaries containing email
and list of scopes, e.g.
[{'email':'default',
'scopes':['compute', ...]}, ...]
Scopes can either be full URLs or short
names. If not provided, use the
'default' service account email and a
scope of 'devstorage.read_only'. Also
accepts the aliases defined in
'gcloud compute'.
:type ex_service_accounts: ``list``
:return: A Node object for the new node.
:rtype: :class:`Node`
"""
with open(script, 'r') as f:
script_data = f.read()
metadata = {'items': [{'key': 'startup-script',
'value': script_data}]}
return self.create_node(name, size, image, location=location,
ex_network=ex_network, ex_tags=ex_tags,
ex_metadata=metadata,
ex_service_accounts=ex_service_accounts)
def attach_volume(self, node, volume, device=None, ex_mode=None,
ex_boot=False):
"""
Attach a volume to a node.
If volume is None, a scratch disk will be created and attached.
:param node: The node to attach the volume to
:type node: :class:`Node`
:param volume: The volume to attach. If none, a scratch disk will be
attached.
:type volume: :class:`StorageVolume` or ``None``
:keyword device: The device name to attach the volume as. Defaults to
volume name.
:type device: ``str``
:keyword ex_mode: Either 'READ_WRITE' or 'READ_ONLY'
:type ex_mode: ``str``
:keyword ex_boot: If true, disk will be attached as a boot disk
:type ex_boot: ``bool``
:return: True if successful
:rtype: ``bool``
"""
volume_data = {}
if volume is None:
volume_data['type'] = 'SCRATCH'
else:
volume_data['type'] = 'PERSISTENT'
volume_data['source'] = volume.extra['selfLink']
volume_data['kind'] = 'compute#attachedDisk'
volume_data['mode'] = ex_mode or 'READ_WRITE'
if device:
volume_data['deviceName'] = device
else:
volume_data['deviceName'] = volume.name
volume_data['boot'] = ex_boot
request = '/zones/%s/instances/%s/attachDisk' % (
node.extra['zone'].name, node.name)
self.connection.async_request(request, method='POST',
data=volume_data)
return True
def detach_volume(self, volume, ex_node=None):
"""
Detach a volume from a node.
:param volume: Volume object to detach
:type volume: :class:`StorageVolume`
:keyword ex_node: Node object to detach volume from (required)
:type ex_node: :class:`Node`
:return: True if successful
:rtype: ``bool``
"""
if not ex_node:
return False
request = '/zones/%s/instances/%s/detachDisk?deviceName=%s' % (
ex_node.extra['zone'].name, ex_node.name, volume.name)
self.connection.async_request(request, method='POST',
data='ignored')
return True
def ex_set_volume_auto_delete(self, volume, node, auto_delete=True):
"""
Sets the auto-delete flag for a volume attached to a node.
:param volume: Volume object to auto-delete
:type volume: :class:`StorageVolume`
:param ex_node: Node object to auto-delete volume from
:type ex_node: :class:`Node`
:keyword auto_delete: Flag to set for the auto-delete value
:type auto_delete: ``bool`` (default True)
:return: True if successful
:rtype: ``bool``
"""
request = '/zones/%s/instances/%s/setDiskAutoDelete' % (
node.extra['zone'].name, node.name
)
delete_params = {
'deviceName': volume,
'autoDelete': auto_delete,
}
self.connection.async_request(request, method='POST',
params=delete_params)
return True
def ex_destroy_address(self, address):
"""
Destroy a static address.
:param address: Address object to destroy
:type address: :class:`GCEAddress`
:return: True if successful
:rtype: ``bool``
"""
request = '/regions/%s/addresses/%s' % (address.region.name,
address.name)
self.connection.async_request(request, method='DELETE')
return True
def ex_delete_image(self, image):
"""
Delete a specific image resource.
:param image: Image object to delete
:type image: ``str`` or :class:`GCENodeImage`
:return: True if successful
:rtype: ``bool``
"""
if not hasattr(image, 'name'):
image = self.ex_get_image(image)
request = '/global/images/%s' % (image.name)
self.connection.async_request(request, method='DELETE')
return True
def ex_deprecate_image(self, image, replacement, state=None):
"""
Deprecate a specific image resource.
:param image: Image object to deprecate
:type image: ``str`` or :class: `GCENodeImage`
:param replacement: Image object to use as a replacement
:type replacement: ``str`` or :class: `GCENodeImage`
:param state: State of the image
:type state: ``str``
:return: True if successful
:rtype: ``bool``
"""
if not hasattr(image, 'name'):
image = self.ex_get_image(image)
if not hasattr(replacement, 'name'):
replacement = self.ex_get_image(replacement)
if state is None:
state = 'DEPRECATED'
possible_states = ['DELETED', 'DEPRECATED', 'OBSOLETE']
if state not in possible_states:
raise ValueError('state must be one of %s'
% ','.join(possible_states))
image_data = {
'state': state,
'replacement': replacement.extra['selfLink'],
}
request = '/global/images/%s/deprecate' % (image.name)
self.connection.request(
request, method='POST', data=image_data).object
return True
def ex_destroy_healthcheck(self, healthcheck):
"""
Destroy a healthcheck.
:param healthcheck: Health check object to destroy
:type healthcheck: :class:`GCEHealthCheck`
:return: True if successful
:rtype: ``bool``
"""
request = '/global/httpHealthChecks/%s' % (healthcheck.name)
self.connection.async_request(request, method='DELETE')
return True
def ex_destroy_firewall(self, firewall):
"""
Destroy a firewall.
:param firewall: Firewall object to destroy
:type firewall: :class:`GCEFirewall`
:return: True if successful
:rtype: ``bool``
"""
request = '/global/firewalls/%s' % (firewall.name)
self.connection.async_request(request, method='DELETE')
return True
def ex_destroy_forwarding_rule(self, forwarding_rule):
"""
Destroy a forwarding rule.
:param forwarding_rule: Forwarding Rule object to destroy
:type forwarding_rule: :class:`GCEForwardingRule`
:return: True if successful
:rtype: ``bool``
"""
request = '/regions/%s/forwardingRules/%s' % (
forwarding_rule.region.name, forwarding_rule.name)
self.connection.async_request(request, method='DELETE')
return True
def ex_destroy_network(self, network):
"""
Destroy a network.
:param network: Network object to destroy
:type network: :class:`GCENetwork`
:return: True if successful
:rtype: ``bool``
"""
request = '/global/networks/%s' % (network.name)
self.connection.async_request(request, method='DELETE')
return True
def destroy_node(self, node, destroy_boot_disk=False):
"""
Destroy a node.
:param node: Node object to destroy
:type node: :class:`Node`
:keyword destroy_boot_disk: If true, also destroy the node's
boot disk. (Note that this keyword is not
accessible from the node's .destroy()
method.)
:type destroy_boot_disk: ``bool``
:return: True if successful
:rtype: ``bool``
"""
request = '/zones/%s/instances/%s' % (node.extra['zone'].name,
node.name)
self.connection.async_request(request, method='DELETE')
if destroy_boot_disk and node.extra['boot_disk']:
node.extra['boot_disk'].destroy()
return True
def ex_destroy_multiple_nodes(self, node_list, ignore_errors=True,
destroy_boot_disk=False, poll_interval=2,
timeout=DEFAULT_TASK_COMPLETION_TIMEOUT):
"""
Destroy multiple nodes at once.
:param node_list: List of nodes to destroy
:type node_list: ``list`` of :class:`Node`
:keyword ignore_errors: If true, don't raise an exception if one or
more nodes fails to be destroyed.
:type ignore_errors: ``bool``
:keyword destroy_boot_disk: If true, also destroy the nodes' boot
disks.
:type destroy_boot_disk: ``bool``
:keyword poll_interval: Number of seconds between status checks.
:type poll_interval: ``int``
:keyword timeout: Number of seconds to wait for all nodes to be
destroyed.
:type timeout: ``int``
:return: A list of boolean values. One for each node. True means
that the node was successfully destroyed.
:rtype: ``list`` of ``bool``
"""
status_list = []
complete = False
start_time = time.time()
for node in node_list:
request = '/zones/%s/instances/%s' % (node.extra['zone'].name,
node.name)
try:
response = self.connection.request(request,
method='DELETE').object
except GoogleBaseError:
self._catch_error(ignore_errors=ignore_errors)
response = None
status = {'node': node,
'node_success': False,
'node_response': response,
'disk_success': not destroy_boot_disk,
'disk_response': None}
status_list.append(status)
while not complete:
if (time.time() - start_time >= timeout):
raise Exception("Timeout (%s sec) while waiting to delete "
"multiple instances")
complete = True
for status in status_list:
# If one of the operations is running, check the status
operation = status['node_response'] or status['disk_response']
delete_disk = False
if operation:
no_errors = True
try:
response = self.connection.request(
operation['selfLink']).object
except GoogleBaseError:
self._catch_error(ignore_errors=ignore_errors)
no_errors = False
response = {'status': 'DONE'}
if response['status'] == 'DONE':
# If a node was deleted, update status and indicate
# that the disk is ready to be deleted.
if status['node_response']:
status['node_response'] = None
status['node_success'] = no_errors
delete_disk = True
else:
status['disk_response'] = None
status['disk_success'] = no_errors
# If we are destroying disks, and the node has been deleted,
# destroy the disk.
if delete_disk and destroy_boot_disk:
boot_disk = status['node'].extra['boot_disk']
if boot_disk:
request = '/zones/%s/disks/%s' % (
boot_disk.extra['zone'].name, boot_disk.name)
try:
response = self.connection.request(
request, method='DELETE').object
except GoogleBaseError:
self._catch_error(ignore_errors=ignore_errors)
no_errors = False
response = None
status['disk_response'] = response
else: # If there is no boot disk, ignore
status['disk_success'] = True
operation = status['node_response'] or status['disk_response']
if operation:
time.sleep(poll_interval)
complete = False
success = []
for status in status_list:
s = status['node_success'] and status['disk_success']
success.append(s)
return success
def ex_destroy_targetpool(self, targetpool):
"""
Destroy a target pool.
:param targetpool: TargetPool object to destroy
:type targetpool: :class:`GCETargetPool`
:return: True if successful
:rtype: ``bool``
"""
request = '/regions/%s/targetPools/%s' % (targetpool.region.name,
targetpool.name)
self.connection.async_request(request, method='DELETE')
return True
def destroy_volume(self, volume):
"""
Destroy a volume.
:param volume: Volume object to destroy
:type volume: :class:`StorageVolume`
:return: True if successful
:rtype: ``bool``
"""
request = '/zones/%s/disks/%s' % (volume.extra['zone'].name,
volume.name)
self.connection.async_request(request, method='DELETE')
return True
def destroy_volume_snapshot(self, snapshot):
"""
Destroy a snapshot.
:param snapshot: Snapshot object to destroy
:type snapshot: :class:`GCESnapshot`
:return: True if successful
:rtype: ``bool``
"""
request = '/global/snapshots/%s' % (snapshot.name)
self.connection.async_request(request, method='DELETE')
return True
def ex_get_address(self, name, region=None):
"""
Return an Address object based on an address name and optional region.
:param name: The name of the address
:type name: ``str``
:keyword region: The region to search for the address in (set to
'all' to search all regions)
:type region: ``str`` :class:`GCERegion` or ``None``
:return: An Address object for the address
:rtype: :class:`GCEAddress`
"""
region = self._set_region(region) or self._find_zone_or_region(
name, 'addresses', region=True, res_name='Address')
request = '/regions/%s/addresses/%s' % (region.name, name)
response = self.connection.request(request, method='GET').object
return self._to_address(response)
def ex_get_healthcheck(self, name):
"""
Return a HealthCheck object based on the healthcheck name.
:param name: The name of the healthcheck
:type name: ``str``
:return: A GCEHealthCheck object
:rtype: :class:`GCEHealthCheck`
"""
request = '/global/httpHealthChecks/%s' % (name)
response = self.connection.request(request, method='GET').object
return self._to_healthcheck(response)
def ex_get_firewall(self, name):
"""
Return a Firewall object based on the firewall name.
:param name: The name of the firewall
:type name: ``str``
:return: A GCEFirewall object
:rtype: :class:`GCEFirewall`
"""
request = '/global/firewalls/%s' % (name)
response = self.connection.request(request, method='GET').object
return self._to_firewall(response)
def ex_get_forwarding_rule(self, name, region=None):
"""
Return a Forwarding Rule object based on the forwarding rule name.
:param name: The name of the forwarding rule
:type name: ``str``
:keyword region: The region to search for the rule in (set to 'all'
to search all regions).
:type region: ``str`` or ``None``
:return: A GCEForwardingRule object
:rtype: :class:`GCEForwardingRule`
"""
region = self._set_region(region) or self._find_zone_or_region(
name, 'forwardingRules', region=True, res_name='ForwardingRule')
request = '/regions/%s/forwardingRules/%s' % (region.name, name)
response = self.connection.request(request, method='GET').object
return self._to_forwarding_rule(response)
def ex_get_image(self, partial_name):
"""
Return an GCENodeImage object based on the name or link provided.
:param partial_name: The name, partial name, or full path of a GCE
image.
:type partial_name: ``str``
:return: GCENodeImage object based on provided information or None if
an image with that name is not found.
:rtype: :class:`GCENodeImage` or ``None``
"""
if partial_name.startswith('https://'):
response = self.connection.request(partial_name, method='GET')
return self._to_node_image(response.object)
image = self._match_images(None, partial_name)
if not image:
if (partial_name.startswith('debian') or
partial_name.startswith('backports')):
image = self._match_images('debian-cloud', partial_name)
elif partial_name.startswith('centos'):
image = self._match_images('centos-cloud', partial_name)
elif partial_name.startswith('sles'):
image = self._match_images('suse-cloud', partial_name)
elif partial_name.startswith('rhel'):
image = self._match_images('rhel-cloud', partial_name)
elif partial_name.startswith('windows'):
image = self._match_images('windows-cloud', partial_name)
elif partial_name.startswith('container-vm'):
image = self._match_images('google-containers', partial_name)
elif partial_name.startswith('coreos'):
image = self._match_images('coreos-cloud', partial_name)
elif partial_name.startswith('opensuse'):
image = self._match_images('opensuse-cloud', partial_name)
return image
def ex_get_network(self, name):
"""
Return a Network object based on a network name.
:param name: The name of the network
:type name: ``str``
:return: A Network object for the network
:rtype: :class:`GCENetwork`
"""
request = '/global/networks/%s' % (name)
response = self.connection.request(request, method='GET').object
return self._to_network(response)
def ex_get_node(self, name, zone=None):
"""
Return a Node object based on a node name and optional zone.
:param name: The name of the node
:type name: ``str``
:keyword zone: The zone to search for the node in. If set to 'all',
search all zones for the instance.
:type zone: ``str`` or :class:`GCEZone` or
:class:`NodeLocation` or ``None``
:return: A Node object for the node
:rtype: :class:`Node`
"""
zone = self._set_zone(zone) or self._find_zone_or_region(
name, 'instances', res_name='Node')
request = '/zones/%s/instances/%s' % (zone.name, name)
response = self.connection.request(request, method='GET').object
return self._to_node(response)
def ex_get_project(self):
"""
Return a Project object with project-wide information.
:return: A GCEProject object
:rtype: :class:`GCEProject`
"""
response = self.connection.request('', method='GET').object
return self._to_project(response)
def ex_get_size(self, name, zone=None):
"""
Return a size object based on a machine type name and zone.
:param name: The name of the node
:type name: ``str``
:keyword zone: The zone to search for the machine type in
:type zone: ``str`` or :class:`GCEZone` or
:class:`NodeLocation` or ``None``
:return: A GCENodeSize object for the machine type
:rtype: :class:`GCENodeSize`
"""
zone = zone or self.zone
if not hasattr(zone, 'name'):
zone = self.ex_get_zone(zone)
request = '/zones/%s/machineTypes/%s' % (zone.name, name)
response = self.connection.request(request, method='GET').object
return self._to_node_size(response)
def ex_get_snapshot(self, name):
"""
Return a Snapshot object based on snapshot name.
:param name: The name of the snapshot
:type name: ``str``
:return: A GCESnapshot object for the snapshot
:rtype: :class:`GCESnapshot`
"""
request = '/global/snapshots/%s' % (name)
response = self.connection.request(request, method='GET').object
return self._to_snapshot(response)
def ex_get_volume(self, name, zone=None):
"""
Return a Volume object based on a volume name and optional zone.
:param name: The name of the volume
:type name: ``str``
:keyword zone: The zone to search for the volume in (set to 'all' to
search all zones)
:type zone: ``str`` or :class:`GCEZone` or :class:`NodeLocation`
or ``None``
:return: A StorageVolume object for the volume
:rtype: :class:`StorageVolume`
"""
zone = self._set_zone(zone) or self._find_zone_or_region(
name, 'disks', res_name='Volume')
request = '/zones/%s/disks/%s' % (zone.name, name)
response = self.connection.request(request, method='GET').object
return self._to_storage_volume(response)
def ex_get_region(self, name):
"""
Return a Region object based on the region name.
:param name: The name of the region.
:type name: ``str``
:return: A GCERegion object for the region
:rtype: :class:`GCERegion`
"""
if name.startswith('https://'):
short_name = self._get_components_from_path(name)['name']
request = name
else:
short_name = name
request = '/regions/%s' % (name)
# Check region cache first
if short_name in self.region_dict:
return self.region_dict[short_name]
# Otherwise, look up region information
response = self.connection.request(request, method='GET').object
return self._to_region(response)
def ex_get_targetpool(self, name, region=None):
"""
Return a TargetPool object based on a name and optional region.
:param name: The name of the target pool
:type name: ``str``
:keyword region: The region to search for the target pool in (set to
'all' to search all regions).
:type region: ``str`` or :class:`GCERegion` or ``None``
:return: A TargetPool object for the pool
:rtype: :class:`GCETargetPool`
"""
region = self._set_region(region) or self._find_zone_or_region(
name, 'targetPools', region=True, res_name='TargetPool')
request = '/regions/%s/targetPools/%s' % (region.name, name)
response = self.connection.request(request, method='GET').object
return self._to_targetpool(response)
def ex_get_zone(self, name):
"""
Return a Zone object based on the zone name.
:param name: The name of the zone.
:type name: ``str``
:return: A GCEZone object for the zone or None if not found
:rtype: :class:`GCEZone` or ``None``
"""
if name.startswith('https://'):
short_name = self._get_components_from_path(name)['name']
request = name
else:
short_name = name
request = '/zones/%s' % (name)
# Check zone cache first
if short_name in self.zone_dict:
return self.zone_dict[short_name]
# Otherwise, look up zone information
try:
response = self.connection.request(request, method='GET').object
except ResourceNotFoundError:
return None
return self._to_zone(response)
def ex_copy_image(self, name, url, description=None):
"""
Copy an image to your image collection.
:param name: The name of the image
:type name: ``str``
:param url: The URL to the image. The URL can start with `gs://`
:param url: ``str``
:param description: The description of the image
:type description: ``str``
:return: NodeImage object based on provided information or None if an
image with that name is not found.
:rtype: :class:`NodeImage` or ``None``
"""
# the URL for an image can start with gs://
if url.startswith('gs://'):
url = url.replace('gs://', 'https://storage.googleapis.com/', 1)
image_data = {
'name': name,
'description': description,
'sourceType': 'RAW',
'rawDisk': {
'source': url,
},
}
request = '/global/images'
self.connection.async_request(request, method='POST',
data=image_data)
return self.ex_get_image(name)
def _ex_connection_class_kwargs(self):
return {'auth_type': self.auth_type,
'project': self.project,
'scopes': self.scopes,
'credential_file': self.credential_file}
def _catch_error(self, ignore_errors=False):
"""
Catch an exception and raise it unless asked to ignore it.
:keyword ignore_errors: If true, just return the error. Otherwise,
raise the error.
:type ignore_errors: ``bool``
:return: The exception that was raised.
:rtype: :class:`Exception`
"""
e = sys.exc_info()[1]
if ignore_errors:
return e
else:
raise e
def _get_components_from_path(self, path):
"""
Return a dictionary containing name & zone/region from a request path.
:param path: HTTP request path (e.g.
'/project/pjt-name/zones/us-central1-a/instances/mynode')
:type path: ``str``
:return: Dictionary containing name and zone/region of resource
:rtype ``dict``
"""
region = None
zone = None
glob = False
components = path.split('/')
name = components[-1]
if components[-4] == 'regions':
region = components[-3]
elif components[-4] == 'zones':
zone = components[-3]
elif components[-3] == 'global':
glob = True
return {'name': name, 'region': region, 'zone': zone, 'global': glob}
def _get_region_from_zone(self, zone):
"""
Return the Region object that contains the given Zone object.
:param zone: Zone object
:type zone: :class:`GCEZone`
:return: Region object that contains the zone
:rtype: :class:`GCERegion`
"""
for region in self.region_list:
zones = [z.name for z in region.zones]
if zone.name in zones:
return region
def _find_zone_or_region(self, name, res_type, region=False,
res_name=None):
"""
Find the zone or region for a named resource.
:param name: Name of resource to find
:type name: ``str``
:param res_type: Type of resource to find.
Examples include: 'disks', 'instances' or 'addresses'
:type res_type: ``str``
:keyword region: If True, search regions instead of zones
:type region: ``bool``
:keyword res_name: The name of the resource type for error messages.
Examples: 'Volume', 'Node', 'Address'
:keyword res_name: ``str``
:return: Zone/Region object for the zone/region for the resource.
:rtype: :class:`GCEZone` or :class:`GCERegion`
"""
if region:
rz = 'region'
else:
rz = 'zone'
rz_name = None
res_name = res_name or res_type
request = '/aggregated/%s' % (res_type)
res_list = self.connection.request(request).object
for k, v in res_list['items'].items():
for res in v.get(res_type, []):
if res['name'] == name:
rz_name = k.replace('%ss/' % (rz), '')
break
if not rz_name:
raise ResourceNotFoundError(
'%s \'%s\' not found in any %s.' % (res_name, name, rz),
None, None)
else:
getrz = getattr(self, 'ex_get_%s' % (rz))
return getrz(rz_name)
def _match_images(self, project, partial_name):
"""
Find the latest image, given a partial name.
For example, providing 'debian-7' will return the image object for the
most recent image with a name that starts with 'debian-7' in the
supplied project. If no project is given, it will search your own
project.
:param project: The name of the project to search for images.
Examples include: 'debian-cloud' and 'centos-cloud'.
:type project: ``str`` or ``None``
:param partial_name: The full name or beginning of a name for an
image.
:type partial_name: ``str``
:return: The latest image object that matches the partial name or None
if no matching image is found.
:rtype: :class:`GCENodeImage` or ``None``
"""
project_images = self.list_images(project)
partial_match = []
for image in project_images:
if image.name == partial_name:
return image
if image.name.startswith(partial_name):
ts = timestamp_to_datetime(image.extra['creationTimestamp'])
if not partial_match or partial_match[0] < ts:
partial_match = [ts, image]
if partial_match:
return partial_match[1]
def _set_region(self, region):
"""
Return the region to use for listing resources.
:param region: A name, region object, None, or 'all'
:type region: ``str`` or :class:`GCERegion` or ``None``
:return: A region object or None if all regions should be considered
:rtype: :class:`GCERegion` or ``None``
"""
region = region or self.region
if region == 'all' or region is None:
return None
if not hasattr(region, 'name'):
region = self.ex_get_region(region)
return region
def _set_zone(self, zone):
"""
Return the zone to use for listing resources.
:param zone: A name, zone object, None, or 'all'
:type region: ``str`` or :class:`GCEZone` or ``None``
:return: A zone object or None if all zones should be considered
:rtype: :class:`GCEZone` or ``None``
"""
zone = zone or self.zone
if zone == 'all' or zone is None:
return None
if not hasattr(zone, 'name'):
zone = self.ex_get_zone(zone)
return zone
def _create_node_req(self, name, size, image, location, network,
tags=None, metadata=None, boot_disk=None,
external_ip='ephemeral', ex_disk_type='pd-standard',
ex_disk_auto_delete=True, ex_service_accounts=None):
"""
Returns a request and body to create a new node. This is a helper
method to support both :class:`create_node` and
:class:`ex_create_multiple_nodes`.
:param name: The name of the node to create.
:type name: ``str``
:param size: The machine type to use.
:type size: :class:`GCENodeSize`
:param image: The image to use to create the node (or, if using a
persistent disk, the image the disk was created from).
:type image: :class:`GCENodeImage`
:param location: The location (zone) to create the node in.
:type location: :class:`NodeLocation` or :class:`GCEZone`
:param network: The network to associate with the node.
:type network: :class:`GCENetwork`
:keyword tags: A list of tags to associate with the node.
:type tags: ``list`` of ``str``
:keyword metadata: Metadata dictionary for instance.
:type metadata: ``dict``
:keyword boot_disk: Persistent boot disk to attach.
:type :class:`StorageVolume`
:keyword external_ip: The external IP address to use. If 'ephemeral'
(default), a new non-static address will be
used. If 'None', then no external address will
be used. To use an existing static IP address,
a GCEAddress object should be passed in.
:type external_ip: :class:`GCEAddress` or ``str`` or None
:keyword ex_disk_type: Specify a pd-standard (default) disk or pd-ssd
for an SSD disk.
:type ex_disk_type: ``str``
:keyword ex_disk_auto_delete: Indicate that the boot disk should be
deleted when the Node is deleted. Set to
True by default.
:type ex_disk_auto_delete: ``bool``
:keyword ex_service_accounts: Specify a list of serviceAccounts when
creating the instance. The format is a
list of dictionaries containing email
and list of scopes, e.g.
[{'email':'default',
'scopes':['compute', ...]}, ...]
Scopes can either be full URLs or short
names. If not provided, use the
'default' service account email and a
scope of 'devstorage.read_only'. Also
accepts the aliases defined in
'gcloud compute'.
:type ex_service_accounts: ``list``
:return: A tuple containing a request string and a node_data dict.
:rtype: ``tuple`` of ``str`` and ``dict``
"""
node_data = {}
node_data['machineType'] = size.extra['selfLink']
node_data['name'] = name
if tags:
node_data['tags'] = {'items': tags}
if metadata:
node_data['metadata'] = metadata
# by default, new instances will match the same serviceAccount and
# scope set in the Developers Console and Cloud SDK
if not ex_service_accounts:
set_scopes = [{
'email': 'default',
'scopes': [self.AUTH_URL + 'devstorage.read_only']
}]
elif not isinstance(ex_service_accounts, list):
raise ValueError("ex_service_accounts field is not a list.")
else:
set_scopes = []
for sa in ex_service_accounts:
if not isinstance(sa, dict):
raise ValueError("ex_service_accounts needs to be a list "
"of dicts, got: '%s - %s'" % (
str(type(sa)), str(sa)))
if 'email' not in sa:
sa['email'] = 'default'
if 'scopes' not in sa:
sa['scopes'] = [self.AUTH_URL + 'devstorage.read_only']
ps = []
for scope in sa['scopes']:
if scope.startswith(self.AUTH_URL):
ps.append(scope)
elif scope in self.SA_SCOPES_MAP:
ps.append(self.AUTH_URL + self.SA_SCOPES_MAP[scope])
else:
ps.append(self.AUTH_URL + scope)
sa['scopes'] = ps
set_scopes.append(sa)
node_data['serviceAccounts'] = set_scopes
if boot_disk:
disks = [{'kind': 'compute#attachedDisk',
'boot': True,
'type': 'PERSISTENT',
'mode': 'READ_WRITE',
'deviceName': boot_disk.name,
'zone': boot_disk.extra['zone'].extra['selfLink'],
'source': boot_disk.extra['selfLink']}]
node_data['disks'] = disks
else:
node_data['image'] = image.extra['selfLink']
ni = [{'kind': 'compute#instanceNetworkInterface',
'network': network.extra['selfLink']}]
if external_ip:
access_configs = [{'name': 'External NAT',
'type': 'ONE_TO_ONE_NAT'}]
if hasattr(external_ip, 'address'):
access_configs[0]['natIP'] = external_ip.address
ni[0]['accessConfigs'] = access_configs
node_data['networkInterfaces'] = ni
request = '/zones/%s/instances' % (location.name)
return request, node_data
def _multi_create_disk(self, status, node_attrs):
"""Create disk for ex_create_multiple_nodes.
:param status: Dictionary for holding node/disk creation status.
(This dictionary is modified by this method)
:type status: ``dict``
:param node_attrs: Dictionary for holding node attribute information.
(size, image, location, ex_disk_type, etc.)
:type node_attrs: ``dict``
"""
disk = None
# Check for existing disk
if node_attrs['use_existing_disk']:
try:
disk = self.ex_get_volume(status['name'],
node_attrs['location'])
except ResourceNotFoundError:
pass
if disk:
status['disk'] = disk
else:
# Create disk and return response object back in the status dict.
# Or, if there is an error, mark as failed.
disk_req, disk_data, disk_params = self._create_vol_req(
None, status['name'], location=node_attrs['location'],
image=node_attrs['image'],
ex_disk_type=node_attrs['ex_disk_type'])
try:
disk_res = self.connection.request(
disk_req, method='POST', data=disk_data,
params=disk_params).object
except GoogleBaseError:
e = self._catch_error(
ignore_errors=node_attrs['ignore_errors'])
error = e.value
code = e.code
disk_res = None
status['disk'] = GCEFailedDisk(status['name'],
error, code)
status['disk_response'] = disk_res
def _multi_check_disk(self, status, node_attrs):
"""Check disk status for ex_create_multiple_nodes.
:param status: Dictionary for holding node/disk creation status.
(This dictionary is modified by this method)
:type status: ``dict``
:param node_attrs: Dictionary for holding node attribute information.
(size, image, location, etc.)
:type node_attrs: ``dict``
"""
error = None
try:
response = self.connection.request(
status['disk_response']['selfLink']).object
except GoogleBaseError:
e = self._catch_error(ignore_errors=node_attrs['ignore_errors'])
error = e.value
code = e.code
response = {'status': 'DONE'}
if response['status'] == 'DONE':
status['disk_response'] = None
if error:
status['disk'] = GCEFailedDisk(status['name'], error, code)
else:
status['disk'] = self.ex_get_volume(status['name'],
node_attrs['location'])
def _multi_create_node(self, status, node_attrs):
"""Create node for ex_create_multiple_nodes.
:param status: Dictionary for holding node/disk creation status.
(This dictionary is modified by this method)
:type status: ``dict``
:param node_attrs: Dictionary for holding node attribute information.
(size, image, location, etc.)
:type node_attrs: ``dict``
"""
# If disk has an error, set the node as failed and return
if hasattr(status['disk'], 'error'):
status['node'] = status['disk']
return
# Create node and return response object in status dictionary.
# Or, if there is an error, mark as failed.
request, node_data = self._create_node_req(
status['name'], node_attrs['size'], node_attrs['image'],
node_attrs['location'], node_attrs['network'], node_attrs['tags'],
node_attrs['metadata'], boot_disk=status['disk'],
external_ip=node_attrs['external_ip'],
ex_service_accounts=node_attrs['ex_service_accounts'])
try:
node_res = self.connection.request(
request, method='POST', data=node_data).object
except GoogleBaseError:
e = self._catch_error(ignore_errors=node_attrs['ignore_errors'])
error = e.value
code = e.code
node_res = None
status['node'] = GCEFailedNode(status['name'],
error, code)
status['node_response'] = node_res
def _multi_check_node(self, status, node_attrs):
"""Check node status for ex_create_multiple_nodes.
:param status: Dictionary for holding node/disk creation status.
(This dictionary is modified by this method)
:type status: ``dict``
:param node_attrs: Dictionary for holding node attribute information.
(size, image, location, etc.)
:type node_attrs: ``dict``
"""
error = None
try:
response = self.connection.request(
status['node_response']['selfLink']).object
except GoogleBaseError:
e = self._catch_error(ignore_errors=node_attrs['ignore_errors'])
error = e.value
code = e.code
response = {'status': 'DONE'}
if response['status'] == 'DONE':
status['node_response'] = None
if error:
status['node'] = GCEFailedNode(status['name'],
error, code)
else:
status['node'] = self.ex_get_node(status['name'],
node_attrs['location'])
def _create_vol_req(self, size, name, location=None, snapshot=None,
image=None, ex_disk_type='pd-standard'):
"""
Assemble the request/data for creating a volume.
Used by create_volume and ex_create_multiple_nodes
:param size: Size of volume to create (in GB). Can be None if image
or snapshot is supplied.
:type size: ``int`` or ``str`` or ``None``
:param name: Name of volume to create
:type name: ``str``
:keyword location: Location (zone) to create the volume in
:type location: ``str`` or :class:`GCEZone` or
:class:`NodeLocation` or ``None``
:keyword snapshot: Snapshot to create image from
:type snapshot: :class:`GCESnapshot` or ``str`` or ``None``
:keyword image: Image to create disk from.
:type image: :class:`GCENodeImage` or ``str`` or ``None``
:keyword ex_disk_type: Specify pd-standard (default) or pd-ssd
:type ex_disk_type: ``str``
:return: Tuple containing the request string, the data dictionary and
the URL parameters
:rtype: ``tuple``
"""
volume_data = {}
params = None
volume_data['name'] = name
if size:
volume_data['sizeGb'] = str(size)
if image:
if not hasattr(image, 'name'):
image = self.ex_get_image(image)
params = {'sourceImage': image.extra['selfLink']}
volume_data['description'] = 'Image: %s' % (
image.extra['selfLink'])
if snapshot:
if not hasattr(snapshot, 'name'):
# Check for full URI to not break backward-compatibility
if snapshot.startswith('https'):
snapshot = self._get_components_from_path(snapshot)['name']
snapshot = self.ex_get_snapshot(snapshot)
snapshot_link = snapshot.extra['selfLink']
volume_data['sourceSnapshot'] = snapshot_link
volume_data['description'] = 'Snapshot: %s' % (snapshot_link)
location = location or self.zone
if not hasattr(location, 'name'):
location = self.ex_get_zone(location)
if ex_disk_type.startswith('https'):
volume_data['type'] = ex_disk_type
else:
volume_data['type'] = 'https://www.googleapis.com/compute/'
volume_data['type'] += '%s/projects/%s/zones/%s/diskTypes/%s' % (
API_VERSION, self.project, location.name, ex_disk_type)
request = '/zones/%s/disks' % (location.name)
return request, volume_data, params
def _to_address(self, address):
"""
Return an Address object from the json-response dictionary.
:param address: The dictionary describing the address.
:type address: ``dict``
:return: Address object
:rtype: :class:`GCEAddress`
"""
extra = {}
region = self.ex_get_region(address['region'])
extra['selfLink'] = address.get('selfLink')
extra['status'] = address.get('status')
extra['creationTimestamp'] = address.get('creationTimestamp')
return GCEAddress(id=address['id'], name=address['name'],
address=address['address'],
region=region, driver=self, extra=extra)
def _to_healthcheck(self, healthcheck):
"""
Return a HealthCheck object from the json-response dictionary.
:param healthcheck: The dictionary describing the healthcheck.
:type healthcheck: ``dict``
:return: HealthCheck object
:rtype: :class:`GCEHealthCheck`
"""
extra = {}
extra['selfLink'] = healthcheck.get('selfLink')
extra['creationTimestamp'] = healthcheck.get('creationTimestamp')
extra['description'] = healthcheck.get('description')
extra['host'] = healthcheck.get('host')
return GCEHealthCheck(
id=healthcheck['id'], name=healthcheck['name'],
path=healthcheck.get('requestPath'), port=healthcheck.get('port'),
interval=healthcheck.get('checkIntervalSec'),
timeout=healthcheck.get('timeoutSec'),
unhealthy_threshold=healthcheck.get('unhealthyThreshold'),
healthy_threshold=healthcheck.get('healthyThreshold'),
driver=self, extra=extra)
def _to_firewall(self, firewall):
"""
Return a Firewall object from the json-response dictionary.
:param firewall: The dictionary describing the firewall.
:type firewall: ``dict``
:return: Firewall object
:rtype: :class:`GCEFirewall`
"""
extra = {}
extra['selfLink'] = firewall.get('selfLink')
extra['creationTimestamp'] = firewall.get('creationTimestamp')
extra['description'] = firewall.get('description')
extra['network_name'] = self._get_components_from_path(
firewall['network'])['name']
network = self.ex_get_network(extra['network_name'])
source_ranges = firewall.get('sourceRanges')
source_tags = firewall.get('sourceTags')
target_tags = firewall.get('targetTags')
return GCEFirewall(id=firewall['id'], name=firewall['name'],
allowed=firewall.get('allowed'), network=network,
source_ranges=source_ranges,
source_tags=source_tags,
target_tags=target_tags,
driver=self, extra=extra)
def _to_forwarding_rule(self, forwarding_rule):
"""
Return a Forwarding Rule object from the json-response dictionary.
:param forwarding_rule: The dictionary describing the rule.
:type forwarding_rule: ``dict``
:return: ForwardingRule object
:rtype: :class:`GCEForwardingRule`
"""
extra = {}
extra['selfLink'] = forwarding_rule.get('selfLink')
extra['portRange'] = forwarding_rule.get('portRange')
extra['creationTimestamp'] = forwarding_rule.get('creationTimestamp')
extra['description'] = forwarding_rule.get('description')
region = self.ex_get_region(forwarding_rule['region'])
targetpool = self.ex_get_targetpool(
self._get_components_from_path(forwarding_rule['target'])['name'])
return GCEForwardingRule(id=forwarding_rule['id'],
name=forwarding_rule['name'], region=region,
address=forwarding_rule.get('IPAddress'),
protocol=forwarding_rule.get('IPProtocol'),
targetpool=targetpool,
driver=self, extra=extra)
def _to_network(self, network):
"""
Return a Network object from the json-response dictionary.
:param network: The dictionary describing the network.
:type network: ``dict``
:return: Network object
:rtype: :class:`GCENetwork`
"""
extra = {}
extra['selfLink'] = network.get('selfLink')
extra['gatewayIPv4'] = network.get('gatewayIPv4')
extra['description'] = network.get('description')
extra['creationTimestamp'] = network.get('creationTimestamp')
return GCENetwork(id=network['id'], name=network['name'],
cidr=network.get('IPv4Range'),
driver=self, extra=extra)
def _to_node_image(self, image):
"""
Return an Image object from the json-response dictionary.
:param image: The dictionary describing the image.
:type image: ``dict``
:return: Image object
:rtype: :class:`GCENodeImage`
"""
extra = {}
extra['preferredKernel'] = image.get('preferredKernel', None)
extra['description'] = image.get('description', None)
extra['creationTimestamp'] = image.get('creationTimestamp')
extra['selfLink'] = image.get('selfLink')
extra['deprecated'] = image.get('deprecated', None)
return GCENodeImage(id=image['id'], name=image['name'], driver=self,
extra=extra)
def _to_node_location(self, location):
"""
Return a Location object from the json-response dictionary.
:param location: The dictionary describing the location.
:type location: ``dict``
:return: Location object
:rtype: :class:`NodeLocation`
"""
return NodeLocation(id=location['id'], name=location['name'],
country=location['name'].split('-')[0],
driver=self)
def _to_node(self, node):
"""
Return a Node object from the json-response dictionary.
:param node: The dictionary describing the node.
:type node: ``dict``
:return: Node object
:rtype: :class:`Node`
"""
public_ips = []
private_ips = []
extra = {}
extra['status'] = node.get('status')
extra['description'] = node.get('description')
extra['zone'] = self.ex_get_zone(node['zone'])
extra['image'] = node.get('image')
extra['machineType'] = node.get('machineType')
extra['disks'] = node.get('disks', [])
extra['networkInterfaces'] = node.get('networkInterfaces')
extra['id'] = node['id']
extra['selfLink'] = node.get('selfLink')
extra['name'] = node['name']
extra['metadata'] = node.get('metadata', {})
extra['tags_fingerprint'] = node['tags']['fingerprint']
extra['scheduling'] = node.get('scheduling', {})
extra['deprecated'] = True if node.get('deprecated', None) else False
for disk in extra['disks']:
if disk.get('boot') and disk.get('type') == 'PERSISTENT':
bd = self._get_components_from_path(disk['source'])
extra['boot_disk'] = self.ex_get_volume(bd['name'], bd['zone'])
if 'items' in node['tags']:
tags = node['tags']['items']
else:
tags = []
extra['tags'] = tags
for network_interface in node.get('networkInterfaces', []):
private_ips.append(network_interface.get('networkIP'))
for access_config in network_interface.get('accessConfigs', []):
public_ips.append(access_config.get('natIP'))
# For the node attributes, use just machine and image names, not full
# paths. Full paths are available in the "extra" dict.
if extra['image']:
image = self._get_components_from_path(extra['image'])['name']
else:
image = None
size = self._get_components_from_path(node['machineType'])['name']
return Node(id=node['id'], name=node['name'],
state=self.NODE_STATE_MAP[node['status']],
public_ips=public_ips, private_ips=private_ips,
driver=self, size=size, image=image, extra=extra)
def _to_node_size(self, machine_type):
"""
Return a Size object from the json-response dictionary.
:param machine_type: The dictionary describing the machine.
:type machine_type: ``dict``
:return: Size object
:rtype: :class:`GCENodeSize`
"""
extra = {}
extra['selfLink'] = machine_type.get('selfLink')
extra['zone'] = self.ex_get_zone(machine_type['zone'])
extra['description'] = machine_type.get('description')
extra['guestCpus'] = machine_type.get('guestCpus')
extra['creationTimestamp'] = machine_type.get('creationTimestamp')
try:
price = self._get_size_price(size_id=machine_type['name'])
except KeyError:
price = None
return GCENodeSize(id=machine_type['id'], name=machine_type['name'],
ram=machine_type.get('memoryMb'),
disk=machine_type.get('imageSpaceGb'),
bandwidth=0, price=price, driver=self, extra=extra)
def _to_project(self, project):
"""
Return a Project object from the json-response dictionary.
:param project: The dictionary describing the project.
:type project: ``dict``
:return: Project object
:rtype: :class:`GCEProject`
"""
extra = {}
extra['selfLink'] = project.get('selfLink')
extra['creationTimestamp'] = project.get('creationTimestamp')
extra['description'] = project.get('description')
metadata = project['commonInstanceMetadata'].get('items')
return GCEProject(id=project['id'], name=project['name'],
metadata=metadata, quotas=project.get('quotas'),
driver=self, extra=extra)
def _to_region(self, region):
"""
Return a Region object from the json-response dictionary.
:param region: The dictionary describing the region.
:type region: ``dict``
:return: Region object
:rtype: :class:`GCERegion`
"""
extra = {}
extra['selfLink'] = region.get('selfLink')
extra['creationTimestamp'] = region.get('creationTimestamp')
extra['description'] = region.get('description')
quotas = region.get('quotas')
zones = [self.ex_get_zone(z) for z in region.get('zones', [])]
# Work around a bug that will occasionally list missing zones in the
# region output
zones = [z for z in zones if z is not None]
deprecated = region.get('deprecated')
return GCERegion(id=region['id'], name=region['name'],
status=region.get('status'), zones=zones,
quotas=quotas, deprecated=deprecated,
driver=self, extra=extra)
def _to_snapshot(self, snapshot):
"""
Return a Snapshot object from the json-response dictionary.
:param snapshot: The dictionary describing the snapshot
:type snapshot: ``dict``
:return: Snapshot object
:rtype: :class:`VolumeSnapshot`
"""
extra = {}
extra['selfLink'] = snapshot.get('selfLink')
extra['creationTimestamp'] = snapshot.get('creationTimestamp')
extra['sourceDisk'] = snapshot.get('sourceDisk')
return GCESnapshot(id=snapshot['id'], name=snapshot['name'],
size=snapshot['diskSizeGb'],
status=snapshot.get('status'), driver=self,
extra=extra)
def _to_storage_volume(self, volume):
"""
Return a Volume object from the json-response dictionary.
:param volume: The dictionary describing the volume.
:type volume: ``dict``
:return: Volume object
:rtype: :class:`StorageVolume`
"""
extra = {}
extra['selfLink'] = volume.get('selfLink')
extra['zone'] = self.ex_get_zone(volume['zone'])
extra['status'] = volume.get('status')
extra['creationTimestamp'] = volume.get('creationTimestamp')
extra['description'] = volume.get('description')
extra['type'] = volume.get('type', 'pd-standard').split('/')[-1]
return StorageVolume(id=volume['id'], name=volume['name'],
size=volume['sizeGb'], driver=self, extra=extra)
def _to_targetpool(self, targetpool):
"""
Return a Target Pool object from the json-response dictionary.
:param targetpool: The dictionary describing the volume.
:type targetpool: ``dict``
:return: Target Pool object
:rtype: :class:`GCETargetPool`
"""
extra = {}
extra['selfLink'] = targetpool.get('selfLink')
extra['description'] = targetpool.get('description')
extra['sessionAffinity'] = targetpool.get('sessionAffinity')
region = self.ex_get_region(targetpool['region'])
healthcheck_list = [self.ex_get_healthcheck(h.split('/')[-1]) for h
in targetpool.get('healthChecks', [])]
node_list = []
for n in targetpool.get('instances', []):
# Nodes that do not exist can be part of a target pool. If the
# node does not exist, use the URL of the node instead of the node
# object.
comp = self._get_components_from_path(n)
try:
node = self.ex_get_node(comp['name'], comp['zone'])
except ResourceNotFoundError:
node = n
node_list.append(node)
return GCETargetPool(id=targetpool['id'], name=targetpool['name'],
region=region, healthchecks=healthcheck_list,
nodes=node_list, driver=self, extra=extra)
def _to_zone(self, zone):
"""
Return a Zone object from the json-response dictionary.
:param zone: The dictionary describing the zone.
:type zone: ``dict``
:return: Zone object
:rtype: :class:`GCEZone`
"""
extra = {}
extra['selfLink'] = zone.get('selfLink')
extra['creationTimestamp'] = zone.get('creationTimestamp')
extra['description'] = zone.get('description')
deprecated = zone.get('deprecated')
return GCEZone(id=zone['id'], name=zone['name'], status=zone['status'],
maintenance_windows=zone.get('maintenanceWindows'),
deprecated=deprecated, driver=self, extra=extra)