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

Merge branch 'feature-servers-api-access' into develop

parents 8bbcbac8 05d32a11
......@@ -41,6 +41,12 @@ Cyclades
* Compute quotas for CPU and memory of running vms.
* Obsolete PUBLIC_USE_POOL setting, since Cyclades manages IP pool for all
type of networks.
* Extend servers info API response with 'SNF:fqdn' attribute, and introduce
CYCLADES_SERVERS_FQDN to set the template for servers FDQN.
* Extend servers info API response with 'SNF:port_forwarding' attribute,
describing port fowarding rules (DNAT) that are applied to vms. The
description of such rules is done via the new CYCLADES_PORT_FORWARDING
setting.
Pithos
------
......
......@@ -107,3 +107,29 @@
# Tune the size of the http connection pool to astakos.
#CYCLADES_ASTAKOSCLIENT_POOLSIZE = 50
#
## Template to use to build the FQDN of VMs. The setting will be formated with
## the id of the VM. If set to 'None' the first public IPv4 or IPv6 address
## of the VM will be used.
#CYCLADES_SERVERS_FQDN = 'snf-%(id)s.vm.example.synnefo.org'
#
## Description of applied port forwarding rules (DNAT) for Cyclades VMs. This
## setting contains a mapping from the port of each VM to a tuple contaning the
## destination IP/hostname and the new port: (host, port). Instead of a tuple a
## python callable object may be used which must return such a tuple. The caller
## will pass to the callable the following positional arguments, in the
## following order:
## * server_id: The ID of the VM in the DB
## * ip_address: The IPv4 address of the public VM NIC
## * fqdn: The FQDN of the VM
## * user: The UUID of the owner of the VM
##
## Here is an example describing the mapping of the SSH port of all VMs to
## the external address 'gate.example.synnefo.org' and port 60000+server_id.
## e.g. iptables -t nat -A prerouting -d gate.example.synnefo.org \
## --dport (61000 # $(VM_ID)) -j DNAT --to-destination $(VM_IP):22
##CYCLADES_PORT_FORWARDING = {
## 22: lambda ip_address, server_id, fqdn, user:
## ("gate.example.synnefo.org", 61000 + server_id),
##}
#CYCLADES_PORT_FORWARDING = {}
......@@ -170,10 +170,73 @@ def vm_to_dict(vm, detail=False):
d["config_drive"] = ""
d["accessIPv4"] = ""
d["accessIPv6"] = ""
fqdn = get_server_fqdn(vm)
d["SNF:fqdn"] = fqdn
d["SNF:port_forwarding"] = get_server_port_forwarding(vm, fqdn)
return d
def get_server_fqdn(vm):
fqdn_setting = settings.CYCLADES_SERVERS_FQDN
if fqdn_setting is None:
public_nics = vm.nics.filter(network__public=True, state="ACTIVE")
# Return the first public IPv4 address if exists
ipv4_nics = public_nics.exclude(ipv4=None)
if ipv4_nics:
return ipv4_nics[0].ipv4
# Else return the first public IPv6 address if exists
ipv6_nics = public_nics.exclude(ipv6=None)
if ipv6_nics:
return ipv6_nics[0].ipv6
return ""
elif isinstance(fqdn_setting, basestring):
return fqdn_setting % {"id": vm.id}
else:
msg = ("Invalid setting: CYCLADES_SERVERS_FQDN."
" Value must be a string.")
raise faults.InternalServerError(msg)
def get_server_port_forwarding(vm, fqdn):
"""Create API 'port_forwarding' attribute from corresponding setting.
Create the 'port_forwarding' API vm attribute based on the corresponding
setting (CYCLADES_PORT_FORWARDING), which can be either a tuple
of the form (host, port) or a callable object returning such tuple. In
case of callable object, must be called with the following arguments:
* ip_address
* server_id
* fqdn
* owner UUID
"""
port_forwarding = {}
for dport, to_dest in settings.CYCLADES_PORT_FORWARDING.items():
if hasattr(to_dest, "__call__"):
public_nics = vm.nics.filter(network__public=True, state="ACTIVE")\
.exclude(ipv4=None).order_by('index')
if public_nics:
vm_ipv4 = public_nics[0].ipv4
else:
vm_ipv4 = None
to_dest = to_dest(vm_ipv4, vm.id, fqdn, vm.userid)
msg = ("Invalid setting: CYCLADES_PORT_FOWARDING."
" Value must be a tuple of two elements (host, port).")
if to_dest is None:
continue
if not isinstance(to_dest, tuple) or len(to_dest) != 2:
raise faults.InternalServerError(msg)
else:
try:
host, port = to_dest
except (TypeError, ValueError):
raise faults.InternalServerError(msg)
port_forwarding[dport] = {"host": host, "port": str(port)}
return port_forwarding
def diagnostics_to_dict(diagnostics):
"""
Extract api data from diagnostics QuerySet.
......
......@@ -43,7 +43,7 @@ from synnefo.logic.utils import get_rsapi_state
from synnefo.cyclades_settings import cyclades_services
from synnefo.lib.services import get_service_path
from synnefo.lib import join_urls
from synnefo import settings
from django.conf import settings
from mock import patch, Mock
......@@ -123,7 +123,7 @@ class ServerAPITest(ComputeAPITest):
user = self.vm2.userid
net = mfactory.NetworkFactory()
nic = mfactory.NetworkInterfaceFactory(machine=self.vm2, network=net,
ipv6="::babe")
ipv6="::babe")
db_vm_meta = mfactory.VirtualMachineMetadataFactory(vm=db_vm)
......@@ -142,18 +142,101 @@ class ServerAPITest(ComputeAPITest):
self.assertEqual(api_nic['firewallProfile'], nic.firewall_profile)
self.assertEqual(api_nic['ipv4'], nic.ipv4)
self.assertEqual(api_nic['ipv6'], nic.ipv6)
self.assertEqual(api_nic['OS-EXT-IPS:type'],"fixed")
self.assertEqual(api_nic['OS-EXT-IPS:type'], "fixed")
self.assertEqual(api_nic['id'], 'nic-%s-%s' % (db_vm.id, nic.index))
api_address = server["addresses"]
self.assertEqual(api_address[str(net.id)],
[{"version": 4, "addr": nic.ipv4, "OS-EXT-IPS:type": "fixed"},
{"version": 6, "addr": nic.ipv6, "OS-EXT-IPS:type": "fixed"}])
self.assertEqual(api_address[str(net.id)], [
{"version": 4, "addr": nic.ipv4, "OS-EXT-IPS:type": "fixed"},
{"version": 6, "addr": nic.ipv6, "OS-EXT-IPS:type": "fixed"}
])
metadata = server['metadata']
self.assertEqual(len(metadata), 1)
self.assertEqual(metadata[db_vm_meta.meta_key], db_vm_meta.meta_value)
self.assertSuccess(response)
def test_server_fqdn(self):
vm = mfactory.VirtualMachineFactory()
with override_settings(settings,
CYCLADES_SERVERS_FQDN="vm.example.org"):
response = self.myget("servers/%d" % vm.id, vm.userid)
server = json.loads(response.content)['server']
self.assertEqual(server["SNF:fqdn"], "vm.example.org")
with override_settings(settings, CYCLADES_SERVERS_FQDN=
"snf-%(id)s.vm.example.org"):
response = self.myget("servers/%d" % vm.id, vm.userid)
server = json.loads(response.content)['server']
self.assertEqual(server["SNF:fqdn"],
"snf-%d.vm.example.org" % vm.id)
with override_settings(settings,
CYCLADES_SERVERS_FQDN=
"snf-%(id)s.vm-%(id)s.example.org"):
response = self.myget("servers/%d" % vm.id, vm.userid)
server = json.loads(response.content)['server']
self.assertEqual(server["SNF:fqdn"], "snf-%d.vm-%d.example.org" %
(vm.id, vm.id))
# No setting, no NICs
with override_settings(settings,
CYCLADES_SERVERS_FQDN=None):
response = self.myget("servers/%d" % vm.id, vm.userid)
server = json.loads(response.content)['server']
self.assertEqual(server["SNF:fqdn"], "")
# IPv6 NIC
nic = mfactory.NetworkInterfaceFactory(machine=vm, ipv4=None,
ipv6="babe::", state="ACTIVE",
network__public=True)
with override_settings(settings,
CYCLADES_SERVERS_FQDN=None):
response = self.myget("servers/%d" % vm.id, vm.userid)
server = json.loads(response.content)['server']
self.assertEqual(server["SNF:fqdn"], nic.ipv6)
# IPv4 NIC
nic = mfactory.NetworkInterfaceFactory(machine=vm,
network__public=True,
state="ACTIVE")
with override_settings(settings,
CYCLADES_SERVERS_FQDN=None):
response = self.myget("servers/%d" % vm.id, vm.userid)
server = json.loads(response.content)['server']
self.assertEqual(server["SNF:fqdn"], nic.ipv4)
def test_server_port_forwarding(self):
vm = mfactory.VirtualMachineFactory()
ports = {
22: ("foo", 61000),
80: lambda ip, id, fqdn, user: ("bar", 61001)}
with override_settings(settings,
CYCLADES_PORT_FORWARDING=ports):
response = self.myget("servers/%d" % vm.id, vm.userid)
server = json.loads(response.content)['server']
self.assertEqual(server["SNF:port_forwarding"],
{"22": {"host": "foo", "port": "61000"},
"80": {"host": "bar", "port": "61001"}})
def _port_from_ip(ip, base):
fields = ip.split('.', 4)
return (base + 256*int(fields[2]) + int(fields[3]))
ports = {
22: lambda ip, id, fqdn, user:
ip and ("gate", _port_from_ip(ip, 10000)) or None}
with override_settings(settings,
CYCLADES_PORT_FORWARDING=ports):
response = self.myget("servers/%d" % vm.id, vm.userid)
server = json.loads(response.content)['server']
self.assertEqual(server["SNF:port_forwarding"], {})
mfactory.NetworkInterfaceFactory(machine=vm, ipv4="192.168.2.2",
network__public=True)
with override_settings(settings,
CYCLADES_PORT_FORWARDING=ports):
response = self.myget("servers/%d" % vm.id, vm.userid)
server = json.loads(response.content)['server']
self.assertEqual(server["SNF:port_forwarding"],
{"22": {"host": "gate", "port": "10514"}})
def test_server_building_nics(self):
db_vm = self.vm2
user = self.vm2.userid
......@@ -204,7 +287,7 @@ class ServerAPITest(ComputeAPITest):
response = self.myget('nonexistent')
self.assertEqual(response.status_code, 400)
try:
error = json.loads(response.content)
json.loads(response.content)
except ValueError:
self.assertTrue(False)
......@@ -256,16 +339,16 @@ class ServerCreateAPITest(ComputeAPITest):
backend=self.backend,
operstate="ACTIVE")
self.request = {
"server": {
"name": "new-server-test",
"userid": "test_user",
"imageRef": 1,
"flavorRef": self.flavor.id,
"metadata": {
"My Server Name": "Apache1"
},
"personality": []
}
"server": {
"name": "new-server-test",
"userid": "test_user",
"imageRef": 1,
"flavorRef": self.flavor.id,
"metadata": {
"My Server Name": "Apache1"
},
"personality": []
}
}
def test_create_server(self, mrapi):
......@@ -284,7 +367,7 @@ class ServerCreateAPITest(ComputeAPITest):
self.assertEqual(api_server['status'], "BUILD")
self.assertEqual(api_server['progress'], 0)
self.assertEqual(api_server['metadata'],
{"My Server Name": "Apache1"})
{"My Server Name": "Apache1"})
self.assertTrue('adminPass' in api_server)
db_vm = VirtualMachine.objects.get(userid='test_user')
......@@ -315,8 +398,10 @@ class ServerCreateAPITest(ComputeAPITest):
request = deepcopy(self.request)
request["server"]["networks"] = [bnet3.network.id, bnet4.network.id]
with override_settings(settings,
DEFAULT_INSTANCE_NETWORKS=["SNF:ANY_PUBLIC", bnet1.network.id,
bnet2.network.id]):
DEFAULT_INSTANCE_NETWORKS=[
"SNF:ANY_PUBLIC",
bnet1.network.id,
bnet2.network.id]):
with mocked_quotaholder():
response = self.mypost('servers', 'test_user',
json.dumps(request), 'json')
......@@ -336,7 +421,7 @@ class ServerCreateAPITest(ComputeAPITest):
request["server"]["floating_ips"] = []
with override_settings(settings,
DEFAULT_INSTANCE_NETWORKS=[bnet2.network.id]):
DEFAULT_INSTANCE_NETWORKS=[bnet2.network.id]):
with mocked_quotaholder():
response = self.mypost('servers', 'test_user',
json.dumps(request), 'json')
......@@ -362,7 +447,7 @@ class ServerCreateAPITest(ComputeAPITest):
with override_settings(settings,
DEFAULT_INSTANCE_NETWORKS=["SNF:ANY_PUBLIC"]):
response = self.mypost('servers', 'test_user',
json.dumps(request), 'json')
json.dumps(request), 'json')
self.assertFault(response, 403, "forbidden")
# test wrong user
request = deepcopy(self.request)
......@@ -389,10 +474,10 @@ class ServerCreateAPITest(ComputeAPITest):
machine=None)
request["server"]["floating_ips"] = [fp1.ipv4, fp2.ipv4]
with override_settings(settings,
DEFAULT_INSTANCE_NETWORKS=[bnet3.network.id]):
DEFAULT_INSTANCE_NETWORKS=[bnet3.network.id]):
with mocked_quotaholder():
response = self.mypost('servers', 'test_user',
json.dumps(request), 'json')
json.dumps(request), 'json')
self.assertEqual(response.status_code, 202)
api_server = json.loads(response.content)['server']
vm = VirtualMachine.objects.get(id=api_server["id"])
......@@ -453,12 +538,14 @@ class ServerMetadataAPITest(ComputeAPITest):
for db_m in metadata:
self.assertEqual(api_metadata[db_m.meta_key], db_m.meta_value)
request = {'metadata':
{'foo': 'bar'},
metadata[0].meta_key: 'bar2'
}
request = {
'metadata': {
'foo': 'bar'
},
metadata[0].meta_key: 'bar2'
}
response = self.mypost('servers/%d/metadata' % vm.id,
vm.userid, json.dumps(request), 'json')
vm.userid, json.dumps(request), 'json')
metadata2 = VirtualMachineMetadata.objects.filter(vm=vm)
response = self.myget('servers/%d/metadata' % vm.id, vm.userid)
self.assertTrue(response.status_code in [200, 203])
......@@ -581,13 +668,13 @@ class ServerActionAPITest(ComputeAPITest):
vm = self.get_vm(flavor=flavor, operstate="BUILD")
request = {'resize': {'flavorRef': flavor.id}}
response = self.mypost('servers/%d/action' % vm.id,
vm.userid, json.dumps(request), 'json')
vm.userid, json.dumps(request), 'json')
self.assertFault(response, 409, "buildInProgress")
# Check same Flavor
vm = self.get_vm(flavor=flavor, operstate="STOPPED")
request = {'resize': {'flavorRef': flavor.id}}
response = self.mypost('servers/%d/action' % vm.id,
vm.userid, json.dumps(request), 'json')
vm.userid, json.dumps(request), 'json')
self.assertBadRequest(response)
# Check flavor with different disk
flavor2 = mfactory.FlavorFactory(disk=1024)
......@@ -595,14 +682,14 @@ class ServerActionAPITest(ComputeAPITest):
vm = self.get_vm(flavor=flavor2, operstate="STOPPED")
request = {'resize': {'flavorRef': flavor3.id}}
response = self.mypost('servers/%d/action' % vm.id,
vm.userid, json.dumps(request), 'json')
vm.userid, json.dumps(request), 'json')
self.assertBadRequest(response)
flavor2 = mfactory.FlavorFactory(disk_template="foo")
flavor3 = mfactory.FlavorFactory(disk_template="baz")
vm = self.get_vm(flavor=flavor2, operstate="STOPPED")
request = {'resize': {'flavorRef': flavor3.id}}
response = self.mypost('servers/%d/action' % vm.id,
vm.userid, json.dumps(request), 'json')
vm.userid, json.dumps(request), 'json')
self.assertBadRequest(response)
# Check success
vm = self.get_vm(flavor=flavor, operstate="STOPPED")
......@@ -612,7 +699,7 @@ class ServerActionAPITest(ComputeAPITest):
request = {'resize': {'flavorRef': flavor4.id}}
mrapi().ModifyInstance.return_value = 42
response = self.mypost('servers/%d/action' % vm.id,
vm.userid, json.dumps(request), 'json')
vm.userid, json.dumps(request), 'json')
self.assertEqual(response.status_code, 202)
vm = VirtualMachine.objects.get(id=vm.id)
self.assertEqual(vm.task_job_id, 42)
......@@ -628,7 +715,7 @@ class ServerActionAPITest(ComputeAPITest):
for action in VirtualMachine.ACTIONS:
request = {action[0]: ""}
response = self.mypost('servers/%d/action' % vm.id,
vm.userid, json.dumps(request), 'json')
vm.userid, json.dumps(request), 'json')
self.assertBadRequest(response)
# however you can destroy
mrapi().DeleteInstance.return_value = 42
......
......@@ -107,3 +107,29 @@ CYCLADES_SERVICE_TOKEN = ''
# endpoints. Set this to False if you deploy cyclades-app/astakos-app on the
# same machine.
CYCLADES_PROXY_USER_SERVICES = True
# Template to use to build the FQDN of VMs. The setting will be formated with
# the id of the VM. If set to 'None' the first public IPv4 or IPv6 address
# of the VM will be used.
CYCLADES_SERVERS_FQDN = 'snf-%(id)s.vm.example.synnefo.org'
# Description of applied port forwarding rules (DNAT) for Cyclades VMs. This
# setting contains a mapping from the port of each VM to a tuple contaning the
# destination IP/hostname and the new port: (host, port). Instead of a tuple a
# python callable object may be used which must return such a tuple. The caller
# will pass to the callable the following positional arguments, in the
# following order:
# * server_id: The ID of the VM in the DB
# * ip_address: The IPv4 address of the public VM NIC
# * fqdn: The FQDN of the VM
# * user: The UUID of the owner of the VM
#
# Here is an example describing the mapping of the SSH port of all VMs to
# the external address 'gate.example.synnefo.org' and port 60000+server_id.
# e.g. iptables -t nat -A prerouting -d gate.example.synnefo.org \
# --dport (61000 + $(VM_ID)) -j DNAT --to-destination $(VM_IP):22
#CYCLADES_PORT_FORWARDING = {
# 22: lambda ip_address, server_id, fqdn, user:
# ("gate.example.synnefo.org", 61000 + server_id),
#}
CYCLADES_PORT_FORWARDING = {}
......@@ -4,7 +4,7 @@ from south.db import db
from south.v2 import DataMigration
from django.db import models
from synnefo import settings
from django.conf import settings
class Migration(DataMigration):
......
......@@ -3,7 +3,7 @@ import datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
from synnefo import settings
from django.conf import settings
class Migration(DataMigration):
......
......@@ -38,7 +38,7 @@ import utils
from contextlib import contextmanager
from hashlib import sha1
from snf_django.lib.api import faults
from synnefo import settings as snf_settings
from django.conf import settings as snf_settings
from aes_encrypt import encrypt_db_charfield, decrypt_db_charfield
from synnefo.db.managers import ForUpdateManager, ProtectedDeleteManager
......
......@@ -33,7 +33,7 @@
from django.test import TestCase
from synnefo import settings
from django.conf import settings
# Import pool tests
from synnefo.db.pools.tests import *
......
......@@ -36,15 +36,15 @@ and implements the message wait and dispatch loops. Actual messages are
handled in the dispatched functions.
"""
from django.core.management import setup_environ
# Fix path to import synnefo settings
import sys
import os
path = os.path.normpath(os.path.join(os.getcwd(), '..'))
sys.path.append(path)
from synnefo import settings
setup_environ(settings)
os.environ['DJANGO_SETTINGS_MODULE'] = 'synnefo.settings'
from django.conf import settings
from django.db import close_connection
......
......@@ -32,7 +32,7 @@ from optparse import make_option
from django.core.management.base import BaseCommand
from synnefo.management.common import get_backend
from synnefo import settings
from django.conf import settings
import datetime
from synnefo.db.models import Backend
......
......@@ -57,13 +57,7 @@ For G, the operating state is True if the machine is up, False otherwise.
from django.core.management import setup_environ
try:
from synnefo import settings
except ImportError:
raise Exception("Cannot import settings, make sure PYTHONPATH contains "
"the parent directory of the Synnefo Django project.")
setup_environ(settings)
from django.conf import settings
import logging
import itertools
......
......@@ -7,7 +7,7 @@ from django.db import transaction
from django.utils import simplejson as json
from snf_django.lib.api import faults
from synnefo import settings
from django.conf import settings
from synnefo import quotas
from synnefo.api import util
from synnefo.logic import backend
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment