diff --git a/glance/README.md b/glance/README.md index 438046f..a3ee9b1 100644 --- a/glance/README.md +++ b/glance/README.md @@ -15,6 +15,7 @@ Glance store driver: Handles glance image endpoint for AWS AMIs [glance_store] default_store = aws stores = aws + show_multiple_locations = true [AWS] secret_key = access_key = diff --git a/glance/create-glance-images.py b/glance/aws/create-glance-images-aws.py similarity index 98% rename from glance/create-glance-images.py rename to glance/aws/create-glance-images-aws.py index 3e78c38..966fec9 100644 --- a/glance/create-glance-images.py +++ b/glance/aws/create-glance-images-aws.py @@ -13,7 +13,8 @@ # limitations under the License. ''' -Run this script as: python create-glance-credentials.py +1. Source your Openstack RC file. +2. Run this script as: python create-glance-images-aws.py ''' import boto3 diff --git a/nova/requirements.txt b/glance/aws/requirements-aws.txt similarity index 100% rename from nova/requirements.txt rename to glance/aws/requirements-aws.txt diff --git a/glance/gce/create-glance-images-gce.py b/glance/gce/create-glance-images-gce.py new file mode 100644 index 0000000..9e35a67 --- /dev/null +++ b/glance/gce/create-glance-images-gce.py @@ -0,0 +1,216 @@ +# Copyright (c) 2017 Platform9 Systems Inc. (http://www.platform9.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +''' +1. Export Openstack RC file +2. Run this script as: python create-glance-credentials.py +''' + +import hashlib +import os +import requests +import sys +import uuid +import keystoneauth1 +import gceutils + +from keystoneauth1 import loading, session +from keystoneclient import client +from six.moves import urllib + + +def get_env_param(env_name): + if env_name in os.environ: + return os.environ[env_name] + raise Exception("%s environment variable not set." % env_name) + + +def get_keystone_session( + auth_url=get_env_param('OS_AUTH_URL'), + project_name=os.environ.get('OS_PROJECT_NAME'), + tenant_name=os.environ.get('OS_TENANT_NAME'), + project_domain_name=os.environ.get('OS_PROJECT_DOMAIN_NAME', + 'default'), # noqa + username=get_env_param('OS_USERNAME'), + user_domain_name=os.environ.get('OS_USER_DOMAIN_NAME', 'default'), + password=get_env_param('OS_PASSWORD')): + + if not project_name: + if not tenant_name: + raise Exception( + "Either OS_PROJECT_NAME or OS_TENANT_NAME is required.") + project_name = tenant_name + + loader = loading.get_plugin_loader('password') + auth = loader.load_from_options( + auth_url=auth_url, project_name=project_name, + project_domain_name=project_domain_name, username=username, + user_domain_name=user_domain_name, password=password) + sess = session.Session(auth=auth) + return sess + + +class GceImages(object): + # Identified by referring, + # 1. https://console.cloud.google.com/compute/images + # 2. https://cloud.google.com/compute/docs/images#os-compute-support + GC_PUBLIC_PROJECTS = [ + 'cos-cloud', 'centos-cloud', 'debian-cloud', 'rhel-cloud', + 'suse-cloud', 'ubuntu-os-cloud', 'windows-cloud', 'coreos-cloud', + 'windows-sql-cloud' + ] + + def __init__(self, service_key_path): + self.gce_svc = gceutils.get_gce_service(service_key_path) + self.img_kind = { + 'RAW': 'raw', + } + self.glance_client = RestClient() + + def get_all_public_images(self): + images = [] + for project in self.GC_PUBLIC_PROJECTS: + images.extend(gceutils.get_images(self.gce_svc, project)) + return images + + def register_gce_images(self): + for image in self.get_all_public_images(): + self.create_image(self._gce_to_ostack_formatter(image)) + + def _get_project(self, gce_link): + # Sample GCE link path: + # https:///compute/v1/projects//global/images/ + parsed_link = urllib.parse.urlparse(gce_link) + project = parsed_link.path.strip('/').split('/')[3] + return project + + def create_image(self, img_data): + """ + Create an OpenStack image. + :param img_data: dict -- Describes GCE Image + :returns: dict -- Response from REST call + :raises: requests.HTTPError + """ + glance_id = img_data['id'] + gce_id = img_data['name'] + print("Creating image: {0}".format(gce_id)) + gce_link = img_data['gce_link'] + gce_project = self._get_project(gce_link) + img_props = { + 'locations': [{ + 'url': + 'gce://%s/%s/%s' % (gce_project, gce_id, glance_id), + 'metadata': { + 'gce_link': gce_link + } + }] + } + try: + resp = self.glance_client.request('POST', '/v2/images', + json=img_data) + resp.raise_for_status() + # Need to update the image in the registry + # with location information so + # the status changes from 'queued' to 'active' + self.update_properties(glance_id, img_props) + print("Created image: {0}".format(gce_id)) + except keystoneauth1.exceptions.http.Conflict: + # ignore error if image already exists + pass + except requests.HTTPError as e: + raise e + + def update_properties(self, imageid, props): + """ + Add or update a set of image properties on an image. + :param imageid: int -- The Ostack image UUID + :param props: dict -- Image properties to update + """ + if not props: + return + patch_body = [] + for name, value in props.iteritems(): + patch_body.append({ + 'op': 'replace', + 'path': '/%s' % name, + 'value': value + }) + resp = self.glance_client.request('PATCH', '/v2/images/%s' % imageid, + json=patch_body) + resp.raise_for_status() + + def _get_image_uuid(self, gce_id): + md = hashlib.md5() + md.update(gce_id) + return str(uuid.UUID(bytes=md.digest())) + + def _gce_to_ostack_formatter(self, gce_img_data): + """ + Converts GCE img data to Openstack img data format. + :param img(dict): gce img data + :return(dict): ostack img data + """ + return { + 'id': self._get_image_uuid(gce_img_data['id']), + 'name': gce_img_data['name'], + 'container_format': 'bare', + 'disk_format': self.img_kind[gce_img_data['sourceType']], + 'visibility': 'public', + 'gce_image_id': gce_img_data['id'], + 'gce_size': gce_img_data['diskSizeGb'], + 'gce_link': gce_img_data['selfLink'] + } + + +class RestClient(object): + def __init__(self): + auth_url = get_env_param('OS_AUTH_URL') + if auth_url.find('v2.0') > 0: + auth_url = auth_url.replace('v2.0', 'v3') + self.auth_url = auth_url + self.region_name = get_env_param('OS_REGION_NAME') + self.sess = get_keystone_session(auth_url=self.auth_url) + self.glance_endpoint = self.get_glance_endpoint() + + def get_glance_endpoint(self): + self.ksclient = client.Client(auth_url=self.auth_url, + session=self.sess) + glance_service_id = self.ksclient.services.list(name='glance')[0].id + glance_url = self.ksclient.endpoints.list( + service=glance_service_id, interface='public', enabled=True, + region=self.region_name)[0].url + return glance_url + + def request(self, method, path, **kwargs): + """ + Make a requests request with retry/relogin on auth failure. + """ + url = self.glance_endpoint + path + headers = self.sess.get_auth_headers() + if method == 'PUT' or method == 'PATCH': + headers['Content-Type'] = '/'.join( + ['application', 'openstack-images-v2.1-json-patch']) + resp = requests.request(method, url, headers=headers, **kwargs) + else: + resp = self.sess.request(url, method, headers=headers, **kwargs) + resp.raise_for_status() + return resp + + +if __name__ == '__main__': + if len(sys.argv) != 2: + print('Usage: {0} '.format(sys.argv[0])) + sys.exit(1) + + gce_images = GceImages(sys.argv[1]) + gce_images.register_gce_images() diff --git a/glance/gce/gceutils.py b/glance/gce/gceutils.py new file mode 100644 index 0000000..abe9a90 --- /dev/null +++ b/glance/gce/gceutils.py @@ -0,0 +1,305 @@ +# 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 six +import time +from oslo_log import log as logging + +from googleapiclient.discovery import build +from oauth2client.client import GoogleCredentials + +LOG = logging.getLogger(__name__) + + +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 + """ + 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 + """ + result = compute.instances().get(project=project, zone=zone, + instance=instance).execute() + return result + + +def get_instance_metadata(compute, project, zone, instance): + """Returns specified instance's metadata + :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 or instance resource, Name of the GCE instance + resource or GCE instance resource + """ + if isinstance(instance, six.string_types): + instance = get_instance(compute, project, zone, instance) + return instance['metadata'] + + +def get_instances_metadata_key(compute, project, zone, instance, key): + """Returns particular key information for specified instance + :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 or instance resource, Name of the GCE instance + resource or GCE instance resource + :param key: string, Key to retrieved from the instance metadata + """ + metadata = get_instance_metadata(compute, project, zone, instance) + if 'items' in metadata: + for item in metadata['items']: + if item['key'] == key: + return item['value'] + return None + + +def get_external_ip(compute, project, zone, instance): + """ Return external IP of GCE instance return empty string otherwise + :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 or instance resource, Name of the GCE instance + resource or GCE instance resource + """ + if isinstance(instance, six.string_types): + instance = get_instance(compute, project, zone, instance) + for interface in instance.get('networkInterfaces', []): + for config in interface.get('accessConfigs', []): + if config['type'] == 'ONE_TO_ONE_NAT' and 'natIP' in config: + return config['natIP'] + return '' + + +def set_instance_metadata(compute, project, zone, instance, items, + operation='add'): + """Perform specified operation on GCE instance metadata + :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 or instance resource, Name of the GCE instance + resource or GCE instance resource + :param items: list, List of items where each item is dictionary having + 'key' and 'value' as its members + Refer following sample list, + [ {'key': 'openstack_id', 'value': '1224555'}, ] + :param operation: string, Operation to perform on instance metadata + """ + if not isinstance(items, list): + raise TypeError( + "set_instance_metadata: items should be instance of list") + metadata = get_instance_metadata(compute, project, zone, instance) + if operation == 'add': + if 'items' in metadata: + metadata['items'].extend(items) + else: + metadata['items'] = items + LOG.info("Adding metadata %s" % (metadata, )) + # TODO: Add del operation if required + return compute.instances().setMetadata(project=project, zone=zone, + instance=instance, + body=metadata).execute() + + +def create_instance(compute, project, zone, name, image_link, machine_link): + """Create GCE instance + :param compute: GCE compute resource object using googleapiclient.discovery + :param project: string, GCE Project Id + :param zone: string, GCE Name of zone + :param name: string, Name of instance to be launched + :param image_link: url, GCE Image link for instance launch + :param machine_link: url, GCE Machine link for instance launch + """ + # source_disk_image = "projects/%s/global/images/%s" % ( + # "debian-cloud", "debian-8-jessie-v20170327") + # machine_link = "zones/%s/machineTypes/n1-standard-1" % zone + LOG.info("Launching instance %s with image %s and machine %s" % + (name, image_link, machine_link)) + + config = { + 'kind': + 'compute#instance', + 'name': + name, + 'machineType': + machine_link, + + # Specify the boot disk and the image to use as a source. + 'disks': [{ + 'boot': True, + 'autoDelete': True, + 'initializeParams': { + 'sourceImage': image_link, + } + }], + + # Specify a network interface with NAT to access the public + # internet. + 'networkInterfaces': [{ + 'network': + 'global/networks/default', + 'accessConfigs': [{ + 'type': 'ONE_TO_ONE_NAT', + 'name': 'External NAT' + }] + }], + + # Allow the instance to access cloud storage and logging. + 'serviceAccounts': [{ + 'email': + 'default', + 'scopes': [ + 'https://www.googleapis.com/auth/devstorage.full_control', + 'https://www.googleapis.com/auth/logging.write', + 'https://www.googleapis.com/auth/compute' + ] + }], + } + + return compute.instances().insert(project=project, zone=zone, + body=config).execute() + + +def delete_instance(compute, project, zone, name): + """Delete GCE instance + :param compute: GCE compute resource object using googleapiclient.discovery + :param project: string, GCE Project Id + :param zone: string, GCE Name of zone + :param name: string, Name of the GCE instance + """ + return compute.instances().delete(project=project, zone=zone, + instance=name).execute() + + +def stop_instance(compute, project, zone, name): + """Stop GCE instance + :param compute: GCE compute resource object using googleapiclient.discovery + :param project: string, GCE Project Id + :param zone: string, GCE Name of zone + :param name: string, Name of the GCE instance + """ + return compute.instances().stop(project=project, zone=zone, + instance=name).execute() + + +def start_instance(compute, project, zone, name): + """Start GCE instance + :param compute: GCE compute resource object using googleapiclient.discovery + :param project: string, GCE Project Id + :param zone: string, GCE Name of zone + :param name: string, Name of the GCE instance + """ + return compute.instances().start(project=project, zone=zone, + instance=name).execute() + + +def reset_instance(compute, project, zone, name): + """Hard reset GCE instance + :param compute: GCE compute resource object using googleapiclient.discovery + :param project: string, GCE Project Id + :param zone: string, GCE Name of zone + :param name: string, Name of the GCE instance + """ + return compute.instances().reset(project=project, zone=zone, + instance=name).execute() + + +def wait_for_operation(compute, project, zone, 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 + """ + operation_name = operation['name'] + if interval < 1: + raise ValueError("wait_for_operation: Interval should be positive") + iterations = timeout / interval + for i in range(iterations): + result = compute.zoneOperations().get( + project=project, zone=zone, operation=operation_name).execute() + if result['status'] == 'DONE': + LOG.info("Operation %s status is %s" % (operation_name, + result['status'])) + if 'error' in result: + raise Exception(result['error']) + return result + time.sleep(interval) + raise Exception( + "wait_for_operation: Operation %s failed to perform in timeout %s" % + (operation_name, timeout)) + + +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 + """ + credentials = GoogleCredentials.from_stream(service_key) + service = build('compute', 'v1', credentials=credentials) + return service + + +def get_machines_info(compute, project, zone): + """Return machine type info from GCE + :param compute: GCE compute resource object using googleapiclient.discovery + :param project: string, GCE Project Id + :param zone: string, GCE Name of zone + """ + response = compute.machineTypes().list(project=project, + zone=zone).execute() + GCE_MAP = { + machine_type['name']: { + 'memory_mb': machine_type['memoryMb'], + 'vcpus': machine_type['guestCpus'] + } + for machine_type in response['items'] + } + return GCE_MAP + + +def get_images(compute, project): + """Return public images info from GCE + :param compute: GCE compute resource object using googleapiclient.discovery + :param project: string, GCE Project Id + """ + response = compute.images().list(project=project, + filter="status eq READY").execute() + if 'items' not in response: + return [] + imgs = filter(lambda img: 'deprecated' not in img, response['items']) + return imgs + + +def get_image(compute, project, name): + """Return public images info from GCE + :param compute: GCE compute resource object using googleapiclient.discovery + :param project: string, GCE Project Id + """ + result = compute.images().get(project=project, image=name).execute() + return result diff --git a/glance/gce/requirements-gce.txt b/glance/gce/requirements-gce.txt new file mode 100644 index 0000000..d8055e0 --- /dev/null +++ b/glance/gce/requirements-gce.txt @@ -0,0 +1 @@ +google-api-python-client diff --git a/glance/glance_store/_drivers/gce.py b/glance/glance_store/_drivers/gce.py new file mode 100644 index 0000000..82bfd9b --- /dev/null +++ b/glance/glance_store/_drivers/gce.py @@ -0,0 +1,160 @@ +# Copyright (c) 2017 Platform9 Systems Inc. (http://www.platform9.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +import gceutils +import glance_store.driver +import glance_store.location +from glance_store import capabilities, exceptions +from glance_store.i18n import _ +from oslo_config import cfg +from oslo_utils import units +from six.moves import urllib + +LOG = logging.getLogger(__name__) + +MAX_REDIRECTS = 5 +STORE_SCHEME = 'gce' + +gce_group = cfg.OptGroup(name='GCE', + title='Options to connect to Google cloud') + +gce_opts = [ + cfg.StrOpt('service_key_path', help='Service key of GCE account', + secret=True), + cfg.StrOpt('zone', help='GCE region'), + cfg.StrOpt('project_id', help='GCE project id'), +] + + +class StoreLocation(glance_store.location.StoreLocation): + """Class describing GCE URI.""" + + def __init__(self, store_specs, conf): + super(StoreLocation, self).__init__(store_specs, conf) + + def process_specs(self): + self.scheme = self.specs.get('scheme', STORE_SCHEME) + self.gce_project = self.specs.get('gce_project') + self.gce_id = self.specs.get('gce_id') + self.glance_id = self.specs.get('glance_id') + + def get_uri(self): + return "{0}://{1}/{2}/{3}".format(self.scheme, self.gce_project, + self.gce_id, self.glance_id) + + def parse_uri(self, uri): + """Parse URLs based on GCE scheme """ + LOG.debug('Parse uri %s' % (uri, )) + if not uri.startswith('%s://' % STORE_SCHEME): + reason = (_("URI %(uri)s must start with %(scheme)s://") % { + 'uri': uri, + 'scheme': STORE_SCHEME + }) + LOG.error(reason) + raise exceptions.BadStoreUri(message=reason) + pieces = urllib.parse.urlparse(uri) + self.scheme = pieces.scheme + gce_project = pieces.netloc + gce_id, glance_id = pieces.path.strip('/').split('/') + parse_params = (gce_project, gce_id, glance_id) + if not all([parse_params]): + raise exceptions.BadStoreUri(uri=uri) + self.gce_project, self.gce_id, self.glance_id = parse_params + + +class Store(glance_store.driver.Store): + """An implementation of the HTTP(S) Backend Adapter""" + + _CAPABILITIES = (capabilities.BitMasks.RW_ACCESS | + capabilities.BitMasks.DRIVER_REUSABLE) + + def __init__(self, conf): + super(Store, self).__init__(conf) + conf.register_group(gce_group) + conf.register_opts(gce_opts, group=gce_group) + self.gce_zone = conf.GCE.zone + self.gce_project = conf.GCE.project_id + self.gce_svc_key = conf.GCE.service_key_path + self.gce_svc = gceutils.get_gce_service(self.gce_svc_key) + LOG.info('Initialized GCE Glance Store driver') + + def get_schemes(self): + """ + :retval tuple: containing valid scheme names to + associate with this store driver + """ + return ('gce', ) + + @capabilities.check + def get(self, location, offset=0, chunk_size=None, context=None): + """ + Takes a `glance_store.location.Location` object that indicates + where to find the image file, and returns a tuple of generator + (for reading the image file) and image_size + + :param location `glance_store.location.Location` object, supplied + from glance_store.location.get_location_from_uri() + """ + yield ('gce://generic', self.get_size(location, context)) + + def get_size(self, location, context=None): + """ + Takes a `glance_store.location.Location` object that indicates + where to find the image file, and returns the size + + :param location `glance_store.location.Location` object, supplied + from glance_store.location.get_location_from_uri() + :retval int: size of image file in bytes + """ + img_data = gceutils.get_image(self.gce_svc, + location.store_location.gce_project, + location.store_location.gce_id) + img_size = int(img_data['diskSizeGb']) * units.Gi + return img_size + + @capabilities.check + def add(self, image_id, image_file, image_size, context=None, + verifier=None): + """ + Stores an image file with supplied identifier to the backend + storage system and returns a tuple containing information + about the stored image. + + :param image_id: The opaque image identifier + :param image_file: The image data to write, as a file-like object + :param image_size: The size of the image data to write, in bytes + + :retval: tuple of URL in backing store, bytes written, checksum + and a dictionary with storage system specific information + :raises: `glance_store.exceptions.Duplicate` if the image already + existed + """ + # Adding images is not suppported yet + raise NotImplementedError + + @capabilities.check + def delete(self, location, context=None): + """Takes a `glance_store.location.Location` object that indicates + where to find the image file to delete + + :param location: `glance_store.location.Location` object, supplied + from glance_store.location.get_location_from_uri() + :raises NotFound if image does not exist + """ + # This method works for GCE public images as we just need to delete + # entry from glance catalog. + # For Private images we will need extra handling here. + LOG.info("Delete image %s" % location.get_store_uri()) diff --git a/glance/requirements.txt b/glance/requirements.txt deleted file mode 100644 index 30ddf82..0000000 --- a/glance/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -boto3 diff --git a/nova/gce/create-nova-flavors-gce.py b/nova/gce/create-nova-flavors-gce.py new file mode 100644 index 0000000..fa20574 --- /dev/null +++ b/nova/gce/create-nova-flavors-gce.py @@ -0,0 +1,84 @@ +# Copyright (c) 2017 Platform9 Systems Inc. (http://www.platform9.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +''' +1. Source openstack RC file +2. python create-nova-flavors-gce.py +''' + +import os +import sys +import gceutils + +from novaclient import client as nova_client +from keystoneauth1 import loading, session + + +def get_env_param(env_name): + if env_name in os.environ: + return os.environ[env_name] + raise Exception("%s environment variable not set." % env_name) + + +def get_keystone_session( + auth_url=get_env_param('OS_AUTH_URL'), + project_name=os.environ.get('OS_PROJECT_NAME'), + tenant_name=os.environ.get('OS_TENANT_NAME'), + project_domain_name=os.environ.get('OS_PROJECT_DOMAIN_NAME', + 'default'), # noqa + username=get_env_param('OS_USERNAME'), + user_domain_name=os.environ.get('OS_USER_DOMAIN_NAME', 'default'), + password=get_env_param('OS_PASSWORD')): + + if not project_name: + if not tenant_name: + raise Exception("Either OS_PROJECT_NAME or OS_TENANT_NAME is required.") + project_name = tenant_name + + loader = loading.get_plugin_loader('password') + auth = loader.load_from_options( + auth_url=auth_url, project_name=project_name, + project_domain_name=project_domain_name, username=username, + user_domain_name=user_domain_name, password=password) + sess = session.Session(auth=auth) + return sess + + +class GceFlavors(object): + def __init__(self, service_key_path, project, zone): + self.gce_svc = gceutils.get_gce_service(service_key_path) + self.project = project + self.zone = zone + + auth_url = get_env_param('OS_AUTH_URL') + if auth_url.find('v2.0') > 0: + auth_url = auth_url.replace('v2.0', 'v3') + self.auth_url = auth_url + self.sess = get_keystone_session(auth_url=self.auth_url) + self.nova_client = nova_client.Client('2', session=self.sess) + + def register_gce_flavors(self): + flavors = gceutils.get_machines_info(self.gce_svc, self.project, + self.zone) + for flavor_name, flavor_info in flavors.iteritems(): + self.nova_client.flavors.create( + flavor_name, flavor_info['memory_mb'], flavor_info['vcpus'], 0) + print("Registered flavor %s" % flavor_name) + + +if __name__ == '__main__': + if len(sys.argv) != 4: + print('Usage: {0} '.format(sys.argv[0])) + sys.exit(1) + gce_flavors = GceFlavors(sys.argv[1], sys.argv[2], sys.argv[3]) + gce_flavors.register_gce_flavors() diff --git a/nova/gce/driver.py b/nova/gce/driver.py index d964fe9..abb80fc 100644 --- a/nova/gce/driver.py +++ b/nova/gce/driver.py @@ -193,8 +193,11 @@ class GCEDriver(driver.ComputeDriver): instance_name = instance.name LOG.info("Creating instance %s as %s on GCE." % (instance.display_name, instance.name)) - operation = gceutils.create_instance(compute, project, zone, - instance_name) + image_link = instance.system_metadata['image_gce_link'] + flavor_name = instance.flavor.name + flavor_link = "zones/%s/machineTypes/%s" % (self.gce_zone, flavor_name) + operation = gceutils.create_instance( + compute, project, zone, instance_name, image_link, flavor_link) gceutils.wait_for_operation(compute, project, zone, operation) gce_instance = gceutils.get_instance(compute, project, zone, instance_name) @@ -446,7 +449,7 @@ class GCEDriver(driver.ComputeDriver): power_state = GCE_STATE_MAP[gce_instance['status']] # TODO: Get correct flavor info - gce_flavor = self.gce_flavor_info['n1-standard-1'] + gce_flavor = self.gce_flavor_info[instance.flavor.name] memory_mb = gce_flavor['memory_mb'] vcpus = gce_flavor['vcpus'] diff --git a/nova/gce/gceutils.py b/nova/gce/gceutils.py index 5b2dd10..abe9a90 100644 --- a/nova/gce/gceutils.py +++ b/nova/gce/gceutils.py @@ -29,6 +29,8 @@ def list_instances(compute, project, zone): :param zone: string, GCE Name of zone """ result = compute.instances().list(project=project, zone=zone).execute() + if 'items' not in result: + return [] return result['items'] @@ -121,16 +123,20 @@ def set_instance_metadata(compute, project, zone, instance, items, body=metadata).execute() -def create_instance(compute, project, zone, name): +def create_instance(compute, project, zone, name, image_link, machine_link): """Create GCE instance :param compute: GCE compute resource object using googleapiclient.discovery :param project: string, GCE Project Id :param zone: string, GCE Name of zone :param name: string, Name of instance to be launched + :param image_link: url, GCE Image link for instance launch + :param machine_link: url, GCE Machine link for instance launch """ - source_disk_image = "projects/%s/global/images/%s" % ( - "debian-cloud", "debian-8-jessie-v20170327") - machine_type = "zones/%s/machineTypes/n1-standard-1" % zone + # source_disk_image = "projects/%s/global/images/%s" % ( + # "debian-cloud", "debian-8-jessie-v20170327") + # machine_link = "zones/%s/machineTypes/n1-standard-1" % zone + LOG.info("Launching instance %s with image %s and machine %s" % + (name, image_link, machine_link)) config = { 'kind': @@ -138,14 +144,14 @@ def create_instance(compute, project, zone, name): 'name': name, 'machineType': - machine_type, + machine_link, # Specify the boot disk and the image to use as a source. 'disks': [{ 'boot': True, 'autoDelete': True, 'initializeParams': { - 'sourceImage': source_disk_image, + 'sourceImage': image_link, } }], @@ -275,3 +281,25 @@ def get_machines_info(compute, project, zone): for machine_type in response['items'] } return GCE_MAP + + +def get_images(compute, project): + """Return public images info from GCE + :param compute: GCE compute resource object using googleapiclient.discovery + :param project: string, GCE Project Id + """ + response = compute.images().list(project=project, + filter="status eq READY").execute() + if 'items' not in response: + return [] + imgs = filter(lambda img: 'deprecated' not in img, response['items']) + return imgs + + +def get_image(compute, project, name): + """Return public images info from GCE + :param compute: GCE compute resource object using googleapiclient.discovery + :param project: string, GCE Project Id + """ + result = compute.images().get(project=project, image=name).execute() + return result