Commit c03a0b05 authored by Christos Stavrakakis's avatar Christos Stavrakakis
Browse files

Merge branch 'feature-floating_ips+resize' into develop

parents 69b8685c 0a84aa41
......@@ -40,3 +40,5 @@ snf-stats-app/synnefo_stats/version.py
astakosclient/astakosclient/version.py
snf-django-lib/snf_django/version.py
snf-branding/synnefo_branding/version.py
*.egg
*.tar.gz
......@@ -6,6 +6,21 @@ Unified Changelog file for Synnefo versions >= 0.13
Since v0.13 most of the Synnefo components have been merged into a single
repository and have aligned versions.
.. _Changelog-0.14next:
v0.14next
=========
Released: UNRELEASED
Cyclades
--------
* Obsolete PUBLIC_USE_POOL setting, since Cyclades manages IP pool for all
type of networks.
Synnefo-wide
------------
.. _Changelog-0.14:
......
......@@ -42,7 +42,6 @@ In `/etc/synnefo/cyclades.conf` add:
.. code-block:: console
MAX_CIDR_BLOCK = 21
PUBLIC_USE_POOL = True
CPU_BAR_GRAPH_URL = 'https://cyclades.example.com/stats/%s/cpu-bar.png'
CPU_TIMESERIES_GRAPH_URL = 'https://cyclades.example.com/stats/%s/cpu-ts.png'
......
......@@ -17,6 +17,13 @@
## Network Configuration
##
#
## List of network IDs. All created instances will get a NIC connected to each
## network of this list. If the special network ID "SNF:ANY_PUBLIC" is used,
## Cyclades will automatically choose a public network and connect the server to
## it.
#DEFAULT_INSTANCE_NETWORKS=["SNF:ANY_PUBLIC"]
#
#
## Maximum allowed network size for private networks.
#MAX_CIDR_BLOCK = 22
#
......@@ -24,12 +31,6 @@
#DEFAULT_MAC_PREFIX = 'aa:00:0'
#DEFAULT_BRIDGE = 'br0'
#
## Boolean value indicating whether synnefo would hold a Pool and allocate IP
## addresses. If this setting is set to False, IP pool management will be
## delegated to Ganeti. If machines have been created with this option as False,
## you must run network reconciliation after turning it to True.
#PUBLIC_USE_POOL = True
#
## Network flavors that users are allowed to create through API requests
#API_ENABLED_NETWORK_FLAVORS = ['MAC_FILTERED']
#
......
# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from socket import getfqdn
from vncauthproxy.client import request_forwarding as request_vnc_forwarding
from django.db import transaction
from django.conf import settings
from django.http import HttpResponse
from django.template.loader import render_to_string
from django.utils import simplejson as json
from snf_django.lib.api import faults
from synnefo.api.util import (random_password, get_vm, get_nic_from_index,
get_network_free_address)
from synnefo.db.models import NetworkInterface
from synnefo.db.pools import EmptyPool
from synnefo.logic import backend
from synnefo.logic.utils import get_rsapi_state
from logging import getLogger
log = getLogger(__name__)
server_actions = {}
network_actions = {}
def server_action(name):
'''Decorator for functions implementing server actions.
`name` is the key in the dict passed by the client.
'''
def decorator(func):
server_actions[name] = func
return func
return decorator
def network_action(name):
'''Decorator for functions implementing network actions.
`name` is the key in the dict passed by the client.
'''
def decorator(func):
network_actions[name] = func
return func
return decorator
@server_action('changePassword')
def change_password(request, vm, args):
# Normal Response Code: 202
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
# unauthorized (401),
# badRequest (400),
# badMediaType(415),
# itemNotFound (404),
# buildInProgress (409),
# overLimit (413)
raise faults.NotImplemented('Changing password is not supported.')
@server_action('reboot')
def reboot(request, vm, args):
# Normal Response Code: 202
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
# unauthorized (401),
# badRequest (400),
# badMediaType(415),
# itemNotFound (404),
# buildInProgress (409),
# overLimit (413)
log.info("Reboot VM %s", vm)
reboot_type = args.get('type', '')
if reboot_type not in ('SOFT', 'HARD'):
raise faults.BadRequest('Malformed Request.')
backend.reboot_instance(vm, reboot_type.lower())
return HttpResponse(status=202)
@server_action('start')
def start(request, vm, args):
# Normal Response Code: 202
# Error Response Codes: serviceUnavailable (503),
# itemNotFound (404)
log.info("Start VM %s", vm)
if args:
raise faults.BadRequest('Malformed Request.')
backend.startup_instance(vm)
return HttpResponse(status=202)
@server_action('shutdown')
def shutdown(request, vm, args):
# Normal Response Code: 202
# Error Response Codes: serviceUnavailable (503),
# itemNotFound (404)
log.info("Shutdown VM %s", vm)
if args:
raise faults.BadRequest('Malformed Request.')
backend.shutdown_instance(vm)
return HttpResponse(status=202)
@server_action('rebuild')
def rebuild(request, vm, args):
# Normal Response Code: 202
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
# unauthorized (401),
# badRequest (400),
# badMediaType(415),
# itemNotFound (404),
# buildInProgress (409),
# serverCapacityUnavailable (503),
# overLimit (413)
raise faults.NotImplemented('Rebuild not supported.')
@server_action('resize')
def resize(request, vm, args):
# Normal Response Code: 202
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
# unauthorized (401),
# badRequest (400),
# badMediaType(415),
# itemNotFound (404),
# buildInProgress (409),
# serverCapacityUnavailable (503),
# overLimit (413),
# resizeNotAllowed (403)
raise faults.NotImplemented('Resize not supported.')
@server_action('confirmResize')
def confirm_resize(request, vm, args):
# Normal Response Code: 204
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
# unauthorized (401),
# badRequest (400),
# badMediaType(415),
# itemNotFound (404),
# buildInProgress (409),
# serverCapacityUnavailable (503),
# overLimit (413),
# resizeNotAllowed (403)
raise faults.NotImplemented('Resize not supported.')
@server_action('revertResize')
def revert_resize(request, vm, args):
# Normal Response Code: 202
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
# unauthorized (401),
# badRequest (400),
# badMediaType(415),
# itemNotFound (404),
# buildInProgress (409),
# serverCapacityUnavailable (503),
# overLimit (413),
# resizeNotAllowed (403)
raise faults.NotImplemented('Resize not supported.')
@server_action('console')
def get_console(request, vm, args):
"""Arrange for an OOB console of the specified type
This method arranges for an OOB console of the specified type.
Only consoles of type "vnc" are supported for now.
It uses a running instance of vncauthproxy to setup proper
VNC forwarding with a random password, then returns the necessary
VNC connection info to the caller.
"""
# Normal Response Code: 200
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
# unauthorized (401),
# badRequest (400),
# badMediaType(415),
# itemNotFound (404),
# buildInProgress (409),
# overLimit (413)
log.info("Get console VM %s", vm)
console_type = args.get('type', '')
if console_type != 'vnc':
raise faults.BadRequest('Type can only be "vnc".')
# Use RAPI to get VNC console information for this instance
if get_rsapi_state(vm) != 'ACTIVE':
raise faults.BadRequest('Server not in ACTIVE state.')
if settings.TEST:
console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
else:
console_data = backend.get_instance_console(vm)
if console_data['kind'] != 'vnc':
message = 'got console of kind %s, not "vnc"' % console_data['kind']
raise faults.ServiceUnavailable(message)
# Let vncauthproxy decide on the source port.
# The alternative: static allocation, e.g.
# sport = console_data['port'] - 1000
sport = 0
daddr = console_data['host']
dport = console_data['port']
password = random_password()
if settings.TEST:
fwd = {'source_port': 1234, 'status': 'OK'}
else:
fwd = request_vnc_forwarding(sport, daddr, dport, password)
if fwd['status'] != "OK":
raise faults.ServiceUnavailable('vncauthproxy returned error status')
# Verify that the VNC server settings haven't changed
if not settings.TEST:
if console_data != backend.get_instance_console(vm):
raise faults.ServiceUnavailable('VNC Server settings changed.')
console = {
'type': 'vnc',
'host': getfqdn(),
'port': fwd['source_port'],
'password': password}
if request.serialization == 'xml':
mimetype = 'application/xml'
data = render_to_string('console.xml', {'console': console})
else:
mimetype = 'application/json'
data = json.dumps({'console': console})
return HttpResponse(data, mimetype=mimetype, status=200)
@server_action('firewallProfile')
def set_firewall_profile(request, vm, args):
# Normal Response Code: 200
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
# unauthorized (401),
# badRequest (400),
# badMediaType(415),
# itemNotFound (404),
# buildInProgress (409),
# overLimit (413)
profile = args.get('profile', '')
log.info("Set VM %s firewall %s", vm, profile)
if profile not in [x[0] for x in NetworkInterface.FIREWALL_PROFILES]:
raise faults.BadRequest("Unsupported firewall profile")
backend.set_firewall_profile(vm, profile)
return HttpResponse(status=202)
@network_action('add')
@transaction.commit_on_success
def add(request, net, args):
# Normal Response Code: 202
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
# unauthorized (401),
# badRequest (400),
# buildInProgress (409),
# badMediaType(415),
# itemNotFound (404),
# overLimit (413)
if net.state != 'ACTIVE':
raise faults.BuildInProgress('Network not active yet')
server_id = args.get('serverRef', None)
if not server_id:
raise faults.BadRequest('Malformed Request.')
vm = get_vm(server_id, request.user_uniq, non_suspended=True)
address = None
if net.dhcp:
# Get a free IP from the address pool.
try:
address = get_network_free_address(net)
except EmptyPool:
raise faults.OverLimit('Network is full')
log.info("Connecting VM %s to Network %s(%s)", vm, net, address)
backend.connect_to_network(vm, net, address)
return HttpResponse(status=202)
@network_action('remove')
@transaction.commit_on_success
def remove(request, net, args):
# Normal Response Code: 202
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
# unauthorized (401),
# badRequest (400),
# badMediaType(415),
# itemNotFound (404),
# overLimit (413)
try: # attachment string: nic-<vm-id>-<nic-index>
server_id = args.get('attachment', None).split('-')[1]
nic_index = args.get('attachment', None).split('-')[2]
except AttributeError:
raise faults.BadRequest("Malformed Request")
except IndexError:
raise faults.BadRequest('Malformed Network Interface Id')
if not server_id or not nic_index:
raise faults.BadRequest('Malformed Request.')
vm = get_vm(server_id, request.user_uniq, non_suspended=True)
nic = get_nic_from_index(vm, nic_index)
log.info("Removing NIC %s from VM %s", str(nic.index), vm)
if nic.dirty:
raise faults.BuildInProgress('Machine is busy.')
else:
vm.nics.all().update(dirty=True)
backend.disconnect_from_network(vm, nic)
return HttpResponse(status=202)
# Copyright 2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from django.conf.urls.defaults import patterns
from django.db import transaction
from django.http import HttpResponse
from django.utils import simplejson as json
from snf_django.lib import api
from snf_django.lib.api import faults, utils
from synnefo.api import util
from synnefo import quotas
from synnefo.db.models import Network, FloatingIP
from logging import getLogger
log = getLogger(__name__)
ips_urlpatterns = patterns(
'synnefo.api.floating_ips',
(r'^(?:/|.json|.xml)?$', 'demux'),
(r'^/(\w+)(?:.json|.xml)?$', 'floating_ip_demux'),
)
pools_urlpatterns = patterns(
"synnefo.api.floating_ips",
(r'^(?:/|.json|.xml)?$', 'list_floating_ip_pools'),
)
def demux(request):
if request.method == 'GET':
return list_floating_ips(request)
elif request.method == 'POST':
return allocate_floating_ip(request)
else:
return api.method_not_allowed(request)
def floating_ip_demux(request, floating_ip_id):
if request.method == 'GET':
return get_floating_ip(request, floating_ip_id)
elif request.method == 'DELETE':
return release_floating_ip(request, floating_ip_id)
else:
return api.method_not_allowed(request)
def ip_to_dict(floating_ip):
machine_id = floating_ip.machine_id
return {"fixed_ip": None,
"id": str(floating_ip.id),
"instance_id": str(machine_id) if machine_id else None,
"ip": floating_ip.ipv4,
"pool": str(floating_ip.network_id)}
@api.api_method(http_method="GET", user_required=True, logger=log,
serializations=["json"])
def list_floating_ips(request):
"""Return user reserved floating IPs"""
log.debug("list_floating_ips")
userid = request.user_uniq
floating_ips = FloatingIP.objects.filter(userid=userid, deleted=False)\
.order_by("id")
floating_ips = map(ip_to_dict, floating_ips)
request.serialization = "json"
data = json.dumps({"floating_ips": floating_ips})
return HttpResponse(data, status=200)
@api.api_method(http_method="GET", user_required=True, logger=log,
serializations=["json"])
def get_floating_ip(request, floating_ip_id):
"""Return information for a floating IP."""
userid = request.user_uniq
try:
floating_ip = FloatingIP.objects.get(id=floating_ip_id,
deleted=False,
userid=userid)
except FloatingIP.DoesNotExist:
raise faults.ItemNotFound("Floating IP '%s' does not exist" %
floating_ip_id)
request.serialization = "json"
data = json.dumps({"floating_ip": ip_to_dict(floating_ip)})
return HttpResponse(data, status=200)
@api.api_method(http_method='POST', user_required=True, logger=log,
serializations=["json"])
@transaction.commit_manually
def allocate_floating_ip(request):
"""Allocate a floating IP."""
req = utils.get_request_dict(request)
log.info('allocate_floating_ip %s', req)
userid = request.user_uniq
pool = req.get("pool", None)
address = req.get("address", None)
machine = None
net_objects = Network.objects.select_for_update()\
.filter(public=True, floating_ip_pool=True,
deleted=False)
try:
if pool is None:
# User did not specified a pool. Choose a random public IP
network, address = util.get_free_ip(net_objects)
else:
try:
network_id = int(pool)
except ValueErrorx:
raise faults.BadRequest("Invalid pool ID.")
network = next((n for n in net_objects if n.id==pool), None)
if network is None:
raise faults.ItemNotFound("Pool '%s' does not exist." % pool)
if address is None:
# User did not specified an IP address. Choose a random one
# Gets X-Lock on IP pool
address = util.get_network_free_address(network)
else:
# User specified an IP address. Check that it is not a used
# floating IP
if FloatingIP.objects.filter(network=network,
deleted=False,
ipv4=address).exists():
msg = "Floating IP '%s' is reserved" % address
raise faults.Conflict(msg)
pool = network.get_pool() # Gets X-Lock
# Check address belongs to pool
if not pool.contains(address):
raise faults.BadRequest("Invalid address")
if pool.is_available(address):
pool.