gce-api/gceapi/api/instance_api.py
Andrey Pavlov 97108dda10 backport fixes to upstream
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
2014-03-15 22:11:48 +04:00

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()