
Fix dependencies add altering with utf8 migrate_version table change place of protocol files add config option for defining regions Use provided sshKeys in instance creation Return requested url in discovery protocol Fixes for cmd scripts Catch nova exception and re-raise GCE exception add two new empty projects for images list fixes for getProject - when cinderclient is old - when neutron is absent fix style and tests Change-Id: Id11d9d7b8613a9f59e054d1fd39ea6dccbe7b4f0
387 lines
16 KiB
Python
387 lines
16 KiB
Python
# Copyright 2013 Cloudscaling Group, 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 express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import random
|
|
import string
|
|
|
|
from gceapi.api import base_api
|
|
from gceapi.api import clients
|
|
from gceapi.api import disk_api
|
|
from gceapi.api import firewall_api
|
|
from gceapi.api import instance_address_api
|
|
from gceapi.api import instance_disk_api
|
|
from gceapi.api import machine_type_api
|
|
from gceapi.api import network_api
|
|
from gceapi.api import operation_api
|
|
from gceapi.api import operation_util
|
|
from gceapi.api import project_api
|
|
from gceapi.api import scopes
|
|
from gceapi.api import utils
|
|
from gceapi import exception
|
|
from gceapi.openstack.common.gettextutils import _
|
|
from gceapi.openstack.common import log as logging
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class API(base_api.API):
|
|
"""GCE Instance API."""
|
|
|
|
KIND = "instance"
|
|
PERSISTENT_ATTRIBUTES = ["id", "description"]
|
|
|
|
_status_map = {
|
|
"UNKNOWN": "STOPPED",
|
|
"ACTIVE": "RUNNING",
|
|
"REBOOT": "RUNNING",
|
|
"HARD_REBOOT": "RUNNING",
|
|
"PASSWORD": "RUNNING",
|
|
"REBUILD": "RUNNING",
|
|
"MIGRATING": "RUNNING",
|
|
"RESIZE": "RUNNING",
|
|
"BUILD": "PROVISIONING",
|
|
"SHUTOFF": "STOPPED",
|
|
"VERIFY_RESIZE": "RUNNING",
|
|
"REVERT_RESIZE": "RUNNING",
|
|
"PAUSED": "STOPPED",
|
|
"SUSPENDED": "STOPPED",
|
|
"RESCUE": "RUNNING",
|
|
"ERROR": "STOPPED",
|
|
"DELETED": "TERMINATED",
|
|
"SOFT_DELETED": "TERMINATED",
|
|
"SHELVED": "STOPPED",
|
|
"SHELVED_OFFLOADED": "STOPPED",
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(API, self).__init__(*args, **kwargs)
|
|
network_api.API()._register_callback(
|
|
base_api._callback_reasons.check_delete,
|
|
self._can_delete_network)
|
|
firewall_api.API()._register_callback(
|
|
base_api._callback_reasons.post_add,
|
|
self._add_secgroup_to_instances)
|
|
firewall_api.API()._register_callback(
|
|
base_api._callback_reasons.pre_delete,
|
|
self._remove_secgroup_from_instances)
|
|
operation_api.API().register_get_progress_method(
|
|
"instance-add",
|
|
self._get_add_item_progress)
|
|
operation_api.API().register_get_progress_method(
|
|
"instance-delete",
|
|
self._get_delete_item_progress)
|
|
operation_api.API().register_get_progress_method(
|
|
"instance-reset",
|
|
self._get_reset_instance_progress)
|
|
|
|
def _get_type(self):
|
|
return self.KIND
|
|
|
|
def _get_persistent_attributes(self):
|
|
return self.PERSISTENT_ATTRIBUTES
|
|
|
|
def get_item(self, context, name, scope=None):
|
|
return self.search_items(context, {"name": name}, scope)[0]
|
|
|
|
def get_items(self, context, scope=None):
|
|
return self.search_items(context, None, scope)
|
|
|
|
def get_scopes(self, context, item):
|
|
return [scopes.ZoneScope(item["OS-EXT-AZ:availability_zone"])]
|
|
|
|
def search_items(self, context, search_opts, scope):
|
|
client = clients.nova(context)
|
|
instances = client.servers.list(search_opts=search_opts)
|
|
|
|
filtered_instances = []
|
|
for instance in instances:
|
|
iscope = getattr(instance, "OS-EXT-AZ:availability_zone")
|
|
if scope is not None and scope.get_name() != iscope:
|
|
continue
|
|
|
|
instance = utils.to_dict(instance)
|
|
instance = self._prepare_instance(client, context, instance)
|
|
db_instance = self._get_db_item_by_id(context, instance["id"])
|
|
self._prepare_item(instance, db_instance)
|
|
filtered_instances.append(instance)
|
|
|
|
if len(filtered_instances) == len(instances) and not search_opts:
|
|
gce_instances = self._get_db_items_dict(context)
|
|
self._purge_db(context, filtered_instances, gce_instances)
|
|
|
|
return filtered_instances
|
|
|
|
def _prepare_instance(self, client, context, instance):
|
|
instance["statusMessage"] = instance["status"]
|
|
instance["status"] = self._status_map.get(
|
|
instance["status"], "STOPPED")
|
|
instance["flavor"]["name"] = machine_type_api.API().get_item_by_id(
|
|
context, instance["flavor"]["id"])["name"]
|
|
|
|
cinder_client = clients.cinder(context)
|
|
volumes = instance["os-extended-volumes:volumes_attached"]
|
|
instance["volumes"] = [utils.to_dict(
|
|
cinder_client.volumes.get(v["id"])) for v in volumes]
|
|
ads = instance_disk_api.API().get_items(context, instance["name"])
|
|
ads = dict((ad["volume_id"], ad) for ad in ads)
|
|
for volume in instance["volumes"]:
|
|
ad = ads.pop(volume["id"], None)
|
|
if not ad:
|
|
name = volume["display_name"]
|
|
ad = instance_disk_api.API().register_item(context,
|
|
instance["name"], volume["id"], name)
|
|
volume["device_name"] = ad["name"]
|
|
# NOTE(apavlov): cleanup unused from db for this instance
|
|
for ad in ads:
|
|
ad = instance_disk_api.API().unregister_item(context,
|
|
instance["name"], ads[ad]["name"])
|
|
|
|
acs = instance_address_api.API().get_items(context, instance["name"])
|
|
acs = dict((ac["addr"], ac) for ac in acs)
|
|
for network in instance["addresses"]:
|
|
for address in instance["addresses"][network]:
|
|
if address["OS-EXT-IPS:type"] == "floating":
|
|
ac = acs.pop(address["addr"], None)
|
|
if not ac:
|
|
ac = instance_address_api.API().register_item(context,
|
|
instance["name"], network, address["addr"],
|
|
None, None)
|
|
address["name"] = ac["name"]
|
|
address["type"] = ac["type"]
|
|
# NOTE(apavlov): cleanup unused from db for this instance
|
|
for ac in acs:
|
|
ac = instance_address_api.API().unregister_item(context,
|
|
instance["name"], acs[ac]["name"])
|
|
|
|
return instance
|
|
|
|
def _can_delete_network(self, context, network):
|
|
client = clients.nova(context)
|
|
instances = client.servers.list(search_opts=None)
|
|
for instance in instances:
|
|
if network["name"] in instance.networks:
|
|
raise exception.NetworkInUse(network_id=network["id"])
|
|
|
|
def _get_instances_with_network(self, context, network_name, scope):
|
|
affected_instances = []
|
|
client = clients.nova(context)
|
|
instances = client.servers.list(search_opts=None)
|
|
for instance in instances:
|
|
if network_name in instance.networks:
|
|
affected_instances.append(instance)
|
|
return affected_instances
|
|
|
|
def _add_secgroup_to_instances(self, context, secgroup, **kwargs):
|
|
network_name = secgroup.get("network_name")
|
|
if not network_name:
|
|
return
|
|
affected_instances = self._get_instances_with_network(
|
|
context, network_name, kwargs.get("scope"))
|
|
# TODO(ft): implement common safe method
|
|
# to run add/remove with exception logging
|
|
for instance in affected_instances:
|
|
try:
|
|
instance.add_security_group(secgroup["name"])
|
|
except Exception:
|
|
LOG.exception(("Failed to add instance "
|
|
"(%s) to security group (%s)"),
|
|
instance.id, secgroup["name"])
|
|
|
|
def _remove_secgroup_from_instances(self, context, secgroup, **kwargs):
|
|
network_name = secgroup.get("network_name")
|
|
if not network_name:
|
|
return
|
|
affected_instances = self._get_instances_with_network(
|
|
context, network_name, kwargs.get("scope"))
|
|
# TODO(ft): implement common safe method
|
|
# to run add/remove with exception logging
|
|
for instance in affected_instances:
|
|
try:
|
|
instance.remove_security_group(secgroup["name"])
|
|
except Exception:
|
|
LOG.exception(("Failed to remove securiy group (%s) "
|
|
"from instance (%s)"),
|
|
secgroup["name"], instance.id)
|
|
|
|
def reset_instance(self, context, scope, name):
|
|
client = clients.nova(context)
|
|
instances = client.servers.list(search_opts={"name": name})
|
|
if not instances or len(instances) != 1:
|
|
raise exception.NotFound
|
|
instance = instances[0]
|
|
operation_util.start_operation(context,
|
|
self._get_reset_instance_progress,
|
|
instance.id)
|
|
instance.reboot("HARD")
|
|
|
|
def delete_item(self, context, name, scope=None):
|
|
client = clients.nova(context)
|
|
instances = client.servers.list(search_opts={"name": name})
|
|
if not instances or len(instances) != 1:
|
|
raise exception.NotFound
|
|
instance = instances[0]
|
|
operation_util.start_operation(context,
|
|
self._get_delete_item_progress,
|
|
instance.id)
|
|
instance.delete()
|
|
instance = utils.to_dict(instance)
|
|
instance = self._prepare_instance(client, context, instance)
|
|
self._delete_db_item(context, instance)
|
|
|
|
ads = instance_disk_api.API().get_items(context, instance["name"])
|
|
for ad in ads:
|
|
ad = instance_disk_api.API().unregister_item(context,
|
|
instance["name"], ad["name"])
|
|
|
|
acs = instance_address_api.API().get_items(context, instance["name"])
|
|
for ac in acs:
|
|
ac = instance_address_api.API().unregister_item(context,
|
|
instance["name"], ac["name"])
|
|
|
|
def add_item(self, context, name, body, scope=None):
|
|
name = body['name']
|
|
client = clients.nova(context)
|
|
|
|
flavor_name = utils._extract_name_from_url(body['machineType'])
|
|
flavor_id = machine_type_api.API().get_item(
|
|
context, flavor_name, scope)["id"]
|
|
|
|
disks = body.get('disks', [])
|
|
disks.sort(None, lambda x: x.get("boot", False), True)
|
|
bdm = dict()
|
|
diskDevice = 0
|
|
for disk in disks:
|
|
device_name = "vd" + string.ascii_lowercase[diskDevice]
|
|
volume_name = utils._extract_name_from_url(disk["source"])
|
|
volume = disk_api.API().get_item(context, volume_name, scope)
|
|
disk["id"] = volume["id"]
|
|
bdm[device_name] = volume["id"]
|
|
diskDevice += 1
|
|
|
|
nics = []
|
|
#NOTE(ft) 'default' security group contains output rules
|
|
#but output rules doesn't configurable by GCE API
|
|
#all outgoing traffic permitted
|
|
#so we support this behaviour
|
|
groups_names = set(['default'])
|
|
acs = dict()
|
|
for net_iface in body['networkInterfaces']:
|
|
net_name = utils._extract_name_from_url(net_iface["network"])
|
|
ac = net_iface.get("accessConfigs")
|
|
if ac:
|
|
if len(ac) > 1:
|
|
msg = _('At most one access config currently supported.')
|
|
raise exception.InvalidRequest(msg)
|
|
else:
|
|
acs[net_name] = ac[0]
|
|
|
|
network = network_api.API().get_item(context, net_name, None)
|
|
nics.append({"net-id": network["id"]})
|
|
for sg in firewall_api.API().get_network_firewalls(
|
|
context, net_name):
|
|
groups_names.add(sg["name"])
|
|
groups_names = list(groups_names)
|
|
|
|
try:
|
|
metadatas = body['metadata']['items']
|
|
except KeyError:
|
|
metadatas = []
|
|
instance_metadata = dict([(x['key'], x['value']) for x in metadatas])
|
|
|
|
ssh_keys = instance_metadata.pop('sshKeys', None)
|
|
if ssh_keys is not None:
|
|
key = ssh_keys.split('\n')[0].split(":")
|
|
key_name = key[0] + "-" + str(random.randint(10000, 99999))
|
|
key_data = key[1]
|
|
client.keypairs.create(key_name, key_data)
|
|
else:
|
|
key_name = project_api.API().get_gce_user_keypair_name(context)
|
|
|
|
try:
|
|
operation_util.start_operation(
|
|
context, self._get_add_item_progress)
|
|
instance = client.servers.create(name, None, flavor_id,
|
|
meta=instance_metadata, min_count=1, max_count=1,
|
|
security_groups=groups_names, key_name=key_name,
|
|
availability_zone=scope.get_name(), block_device_mapping=bdm,
|
|
nics=nics)
|
|
if not acs:
|
|
operation_util.set_item_id(context, instance.id)
|
|
finally:
|
|
if ssh_keys is not None:
|
|
client.keypairs.delete(key_name)
|
|
|
|
for disk in disks:
|
|
instance_disk_api.API().register_item(context, name,
|
|
disk["id"], disk["deviceName"])
|
|
|
|
instance = utils.to_dict(client.servers.get(instance.id))
|
|
instance = self._prepare_instance(client, context, instance)
|
|
if "description" in body:
|
|
instance["description"] = body["description"]
|
|
instance = self._add_db_item(context, instance)
|
|
|
|
if acs:
|
|
operation_util.continue_operation(
|
|
context,
|
|
lambda: self._add_access_config(context, instance,
|
|
scope, acs))
|
|
|
|
return instance
|
|
|
|
def _add_access_config(self, context, instance, scope, acs):
|
|
progress = self._get_add_item_progress(context, instance["id"])
|
|
if progress is None or not operation_api.is_final_progress(progress):
|
|
return progress
|
|
|
|
client = clients.nova(context)
|
|
try:
|
|
instance = client.servers.get(instance["id"])
|
|
except clients.novaclient.exceptions.NotFound:
|
|
return operation_api.gef_final_progress()
|
|
|
|
for net in acs:
|
|
ac = acs[net]
|
|
instance_address_api.API().add_item(context, instance.name,
|
|
net, ac.get("natIP"), ac.get("type"), ac.get("name"))
|
|
return operation_api.gef_final_progress()
|
|
|
|
def _get_add_item_progress(self, context, instance_id):
|
|
client = clients.nova(context)
|
|
try:
|
|
instance = client.servers.get(instance_id)
|
|
except clients.novaclient.exceptions.NotFound:
|
|
return operation_api.gef_final_progress()
|
|
if instance.status != "BUILD":
|
|
return operation_api.gef_final_progress(instance.status == "ERROR")
|
|
|
|
def _get_delete_item_progress(self, context, instance_id):
|
|
client = clients.nova(context)
|
|
try:
|
|
instance = client.servers.get(instance_id)
|
|
except clients.novaclient.exceptions.NotFound:
|
|
return operation_api.gef_final_progress()
|
|
if getattr(instance, "OS-EXT-STS:task_state") != "deleting":
|
|
return operation_api.gef_final_progress(
|
|
instance.status != "DELETED")
|
|
|
|
def _get_reset_instance_progress(self, context, instance_id):
|
|
client = clients.nova(context)
|
|
try:
|
|
instance = client.servers.get(instance_id)
|
|
except clients.novaclient.exceptions.NotFound:
|
|
return operation_api.gef_final_progress()
|
|
if instance.status != "HARD_REBOOT":
|
|
return operation_api.gef_final_progress()
|