""" Copyright (c) 2017 Platform9 Systems Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. See the License for the specific language governing permissions and limitations under the License. """ import os import time import uuid from googleapiclient.discovery import build from oauth2client.client import GoogleCredentials from oslo_log import log as logging from oslo_utils import reflection from neutron._i18n import _ from neutron_lib import exceptions as e from oslo_service import loopingcall from six.moves import urllib LOG = logging.getLogger(__name__) class _FixedIntervalWithTimeoutLoopingCall(loopingcall.LoopingCallBase): """A fixed interval looping call with timeout checking mechanism.""" _RUN_ONLY_ONE_MESSAGE = _("A fixed interval looping call with timeout" " checking and can only run one function at" " at a time") _KIND = _('Fixed interval looping call with timeout checking.') def start(self, interval, initial_delay=None, stop_on_exception=True, timeout=0): start_time = time.time() def _idle_for(result, elapsed): delay = round(elapsed - interval, 2) if delay > 0: func_name = reflection.get_callable_name(self.f) LOG.warning('Function %(func_name)r run outlasted ' 'interval by %(delay).2f sec', {'func_name': func_name, 'delay': delay}) elapsed_time = time.time() - start_time if timeout > 0 and elapsed_time > timeout: raise loopingcall.LoopingCallTimeOut( _('Looping call timed out after %.02f seconds') % elapsed_time) return -delay if delay < 0 else 0 return self._start(_idle_for, initial_delay=initial_delay, stop_on_exception=stop_on_exception) # Currently, default oslo.service version(newton) is 1.16.0. # Once we upgrade oslo.service >= 1.19.0, we can remove temporary # definition _FixedIntervalWithTimeoutLoopingCall if not hasattr(loopingcall, 'FixedIntervalWithTimeoutLoopingCall'): loopingcall.FixedIntervalWithTimeoutLoopingCall = \ _FixedIntervalWithTimeoutLoopingCall class GceOperationError(Exception): pass class GceResourceNotFound(e.NotFound): message = _("GCE Resource %(name)s %(identifier)s was not found") class GceServiceKeyNotFound(e.NotFound): message = _("GCE service key was not found at %(path)s location") def list_instances(compute, project, zone): """Returns list of GCE instance resources for specified project :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param zone: string, GCE Name of zone :return: Instances information :rtype: list """ result = compute.instances().list(project=project, zone=zone).execute() if 'items' not in result: return [] return result['items'] def get_instance(compute, project, zone, instance): """Get GCE instance information :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param zone: string, GCE Name of zone :param instance: string, Name of the GCE instance resource :return: Instance information :rtype: dict """ result = compute.instances().get(project=project, zone=zone, instance=instance).execute() return result def wait_for_operation(compute, project, operation, interval=1, timeout=60): """Wait for GCE operation to complete, raise error if operation failure :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param zone: string, GCE Name of zone :param operation: object, Operation resource obtained by calling GCE API :param interval: int, Time period(seconds) between two GCE operation checks :param timeout: int, Absoulte time period(seconds) to monitor GCE operation """ def watch_operation(name, request): result = request.execute() if result['status'] == 'DONE': LOG.info("Operation %s status is %s" % (name, result['status'])) if 'error' in result: raise GceOperationError(result['error']) raise loopingcall.LoopingCallDone() operation_name = operation['name'] if 'zone' in operation: zone = operation['zone'].split('/')[-1] monitor_request = compute.zoneOperations().get( project=project, zone=zone, operation=operation_name) elif 'region' in operation: region = operation['region'].split('/')[-1] monitor_request = compute.regionOperations().get( project=project, region=region, operation=operation_name) else: monitor_request = compute.globalOperations().get( project=project, operation=operation_name) timer = loopingcall.FixedIntervalWithTimeoutLoopingCall( watch_operation, operation_name, monitor_request) timer.start(interval=interval, timeout=timeout).wait() def get_gce_service(service_key): """Returns GCE compute resource object for interacting with GCE API :param service_key: string, Path of service key obtained from https://console.cloud.google.com/apis/credentials :return: :class:`Resource ` object :rtype: googleapiclient.discovery.Resource """ if not os.path.exists(service_key): raise GceServiceKeyNotFound(path=service_key) credentials = GoogleCredentials.from_stream(service_key) service = build('compute', 'beta', credentials=credentials) return service def create_network(compute, project, name): """Create network in GCE :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param name: string, GCE Name of network :return: Operation information :rtype: dict """ body = {'autoCreateSubnetworks': False, 'name': name} return compute.networks().insert(project=project, body=body).execute() def get_network(compute, project, name): """Get info of network in GCE :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param name: string, GCE Name of network :return: GCE Network information :rtype: dict """ result = compute.networks().get(project=project, network=name).execute() return result def create_subnet(compute, project, region, name, ipcidr, network_link): """Create subnet with particular GCE network :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param region: string, GCE region :param name: string, Subnet name :param ipcidr: string, IP CIDR for subnet :param network_link: url, GCE network resource url :return: Operation information :rtype: dict """ body = { 'privateIpGoogleAccess': False, 'name': name, 'ipCidrRange': ipcidr, 'network': network_link } return compute.subnetworks().insert(project=project, region=region, body=body).execute() def delete_subnet(compute, project, region, name): """Delete subnet in GCE :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param region: string, GCE region :param name: string, Subnet name :return: Operation information :rtype: dict """ return compute.subnetworks().delete(project=project, region=region, subnetwork=name).execute() def delete_network(compute, project, name): """Delete network in GCE :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param name: string, GCE network name :return: Operation information :rtype: dict """ return compute.networks().delete(project=project, network=name).execute() def create_static_ip(compute, project, region, name): """Create global static IP :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param region: string, GCE region :param name: string, Static IP name :return: Operation information :rtype: dict """ return compute.addresses().insert(project=project, region=region, body={ 'name': name, }).execute() def get_static_ip(compute, project, region, name): """Get static IP :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param region: string, GCE region :param name: string, Static IP name :return: GCE static IP information :rtype: dict """ return compute.addresses().get(project=project, region=region, address=name).execute() def delete_static_ip(compute, project, region, name): """Delete static IP :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param region: string, GCE region :param name: string, Static IP name :return: Operation information :rtype: dict """ return compute.addresses().delete(project=project, region=region, address=name).execute() def get_floatingip(compute, project, region, ip): """Get details of static IP in GCE :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param region: string, GCE region :param ip: string, GCE Static IP :return: GCE address information :rtype: dict """ query = 'address eq %s' % ip result = compute.addresses().list(project=project, region=region, filter=query).execute() if 'items' in result and len(result['items']) == 1: return result['items'][0] raise GceResourceNotFound(name='Floating IP', identifier=ip) def allocate_floatingip(compute, project, region): """Get global static IP in GCE :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param region: string, GCE region :return: GCE static IP :rtype: str """ name = 'ip-' + str(uuid.uuid4()) operation = create_static_ip(compute, project, region, name) wait_for_operation(compute, project, operation) address = get_static_ip(compute, project, region, name) return address['address'] def delete_floatingip(compute, project, region, ip): """Delete particular static IP :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param region: string, GCE region :param ip: string, GCE Static IP """ address = get_floatingip(compute, project, region, ip) name = address['name'] operation = delete_static_ip(compute, project, region, name) wait_for_operation(compute, project, operation) def assign_floatingip(compute, project, zone, fixedip, floatingip): """Assign static IP to interface with mentioned fixed IP :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param zone: string, GCE zone :param fixedip: string, GCE private IP from private network :param floatingip: string, GCE static IP """ instances = list_instances(compute, project, zone) instance_name = None for instance in instances: for interface in instance['networkInterfaces']: if interface['networkIP'] == fixedip: instance_name = instance['name'] interface_name = interface['name'] break if not instance_name: raise GceResourceNotFound(name='Instance with fixed IP', identifier=fixedip) LOG.info('Assigning floating ip %s to instance %s' % (floatingip, instance_name)) operation = compute.instances().addAccessConfig( project=project, zone=zone, instance=instance_name, networkInterface=interface_name, body={ 'type': 'ONE_TO_ONE_NAT', 'name': 'External NAT', 'natIP': floatingip }).execute() wait_for_operation(compute, project, operation) def release_floatingip(compute, project, zone, floatingip): """Release GCE static IP from instances using it :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param zone: string, GCE zone :param floatingip: string, GCE static IP """ address = get_floatingip(compute, project, zone, floatingip) for user in address.get('users', []): # Parse instance info # Eg. /compute/v1/projects//zones//instances/ items = urllib.parse.urlparse(user).path.strip('/').split('/') if len(items) < 4 or items[-2] != 'instances': LOG.warning('Unknown referrer %s to GCE static IP %s' % (user, floatingip)) continue instance, zone = items[-1], items[-3] instance_info = get_instance(compute, project, zone, instance) for interface in instance_info['networkInterfaces']: for accessconfig in interface.get('accessConfigs', []): if accessconfig.get('natIP') == floatingip: LOG.info('Releasing %s from instance %s' % (floatingip, instance)) operation = compute.instances().deleteAccessConfig( project=project, zone=zone, instance=instance, accessConfig=accessconfig['name'], networkInterface=interface['name']).execute() wait_for_operation(compute, project, operation) def create_firewall_rule(compute, project, body): """Create firewall rule in GCE :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param body: dict, Information required for creating firewall Refer format at https://developers.google.com/resources/api-libraries/documentation/compute /beta/python/latest/compute_beta.firewalls.html#insert :return: Operation information :rtype: dict """ return compute.firewalls().insert(project=project, body=body).execute() def update_firewall_rule(compute, project, name, body): """Update existing firewall rule in GCE :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param name: string, GCE firewall name :param body: dict, Information required for updating firewall Refer format at https://developers.google.com/resources/api-libraries/documentation/ compute/beta/python/latest/compute_beta.firewalls.html#update :return: Operation information :rtype: dict """ return compute.firewalls().update(project=project, firewall=name, body=body).execute() def delete_firewall_rule(compute, project, name): """Delete firewall rule in GCE :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param name: string, GCE firewall name :return: Operation information :rtype: dict """ return compute.firewalls().delete(project=project, firewall=name).execute() def get_firewall_rule(compute, project, name): """Get firewall rule info in GCE :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param name: string, GCE firewall name :return: Firewall info :rtype: dict """ return compute.firewalls().get(project=project, firewall=name).execute()