Add Glance Store Driver for GCE
1. Consists of Glance store Driver and mechanism to discover GCE images and them to Glance catalog as Glance images 2. Pick up correct Glance flavor in instance spawn 3. Mechanism to populate nova flavors from GCE machine info Implements: blueprint gce-support Change-Id: Iab71206b760ed6f516484690030d9ecb6c891e68 Signed-off-by: Sanket <sanket@infracloud.io>
This commit is contained in:
parent
ccbbe69a8b
commit
9a11c06e3b
@ -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 = <your aws secret access key>
|
||||
access_key = <your aws access key>
|
||||
|
@ -13,7 +13,8 @@
|
||||
# limitations under the License.
|
||||
|
||||
'''
|
||||
Run this script as: python create-glance-credentials.py <access-key> <secret-key> <region-name>
|
||||
1. Source your Openstack RC file.
|
||||
2. Run this script as: python create-glance-images-aws.py <access-key> <secret-key> <region-name>
|
||||
'''
|
||||
|
||||
import boto3
|
216
glance/gce/create-glance-images-gce.py
Normal file
216
glance/gce/create-glance-images-gce.py
Normal file
@ -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 <service-key-path>
|
||||
'''
|
||||
|
||||
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://<domain>/compute/v1/projects/<project>/global/images/<image>
|
||||
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} <service_key_path>'.format(sys.argv[0]))
|
||||
sys.exit(1)
|
||||
|
||||
gce_images = GceImages(sys.argv[1])
|
||||
gce_images.register_gce_images()
|
305
glance/gce/gceutils.py
Normal file
305
glance/gce/gceutils.py
Normal file
@ -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
|
1
glance/gce/requirements-gce.txt
Normal file
1
glance/gce/requirements-gce.txt
Normal file
@ -0,0 +1 @@
|
||||
google-api-python-client
|
160
glance/glance_store/_drivers/gce.py
Normal file
160
glance/glance_store/_drivers/gce.py
Normal file
@ -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())
|
@ -1 +0,0 @@
|
||||
boto3
|
84
nova/gce/create-nova-flavors-gce.py
Normal file
84
nova/gce/create-nova-flavors-gce.py
Normal file
@ -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 <service_key_path> <project> <zone>
|
||||
'''
|
||||
|
||||
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} <service_key_path> <project> <zone>'.format(sys.argv[0]))
|
||||
sys.exit(1)
|
||||
gce_flavors = GceFlavors(sys.argv[1], sys.argv[2], sys.argv[3])
|
||||
gce_flavors.register_gce_flavors()
|
@ -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']
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user