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

Merge branch 'feature-management-commands' into develop

parents a5bd4611 29c1dfb5
......@@ -76,6 +76,13 @@ Cyclades
that hosted only archipelago backends. Instead allocation is based on which
disk-templates are enabled in each backend.
* Implement 'snf-manage server-remove' management command.
* Move reconciliation of IP pools from 'reconcile-networks' to
'reconcile-pools'. The IP pool reconciliation does not reconcile the IP
pools with Ganeti. Instead it checks if the pool is consistent with the
IPs that are used by instances.
* Do not automatically release externally reserved IPs if they are released
from a Ganeti backend. Management of externally reserved IPs must be
performed from Cyclades with 'network-modify' command.
Pithos
------
......
......@@ -35,75 +35,42 @@ from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
from synnefo.db.models import (Network, Backend, BackendNetwork,
pooled_rapi_client)
from synnefo.db.models import (Backend, BackendNetwork, pooled_rapi_client)
from synnefo.management.common import (get_network, get_backend)
from snf_django.management.utils import parse_bool
from synnefo.logic import networks
from synnefo.logic.backend import create_network, delete_network
HELP_MSG = """Modify a network.
This management command will only modify the state of the network in Cyclades
DB. The state of the network in the Ganeti backends will remain unchanged. You
should manually modify the network in all the backends, to synchronize the
state of DB and Ganeti.
The only exception is add_reserved_ips and remove_reserved_ips options, which
modify the IP pool in the Ganeti backends.
"""
from synnefo.logic import networks, backend as backend_mod
from django.db import transaction
class Command(BaseCommand):
args = "<network id>"
help = HELP_MSG
output_transaction = True
help = "Modify a network."
option_list = BaseCommand.option_list + (
make_option(
'--name',
dest='name',
metavar='NAME',
help="Set network's name"),
help="Rename a network"),
make_option(
'--userid',
dest='userid',
help="Set the userid of the network owner"),
make_option(
'--subnet',
dest='subnet',
help="Set network's subnet"),
make_option(
'--gateway',
dest='gateway',
help="Set network's gateway"),
make_option(
'--subnet6',
dest='subnet6',
help="Set network's IPv6 subnet"),
help="Change the owner of the network."),
make_option(
'--gateway6',
dest='gateway6',
help="Set network's IPv6 gateway"),
make_option(
'--dhcp',
dest='dhcp',
"--drained",
dest="drained",
metavar="True|False",
choices=["True", "False"],
help="Set if network will use nfdhcp"),
make_option(
'--state',
dest='state',
metavar='STATE',
help="Set network's state"),
make_option(
'--link',
dest='link',
help="Set the connectivity link"),
help="Set as drained to exclude for IP allocation."
" Only used for public networks."),
make_option(
'--mac-prefix',
dest="mac_prefix",
help="Set the MAC prefix"),
"--floating-ip-pool",
dest="floating_ip_pool",
metavar="True|False",
choices=["True", "False"],
help="Convert network to a floating IP pool. During this"
" conversation the network will be created to all"
" available Ganeti backends."),
make_option(
'--add-reserved-ips',
dest="add_reserved_ips",
......@@ -112,84 +79,74 @@ class Command(BaseCommand):
'--remove-reserved-ips',
dest="remove_reserved_ips",
help="Comma seperated list of IPs to externally release."),
make_option(
"--drained",
dest="drained",
metavar="True|False",
choices=["True", "False"],
help="Set as drained to exclude for IP allocation."
" Only used for public networks."),
make_option(
"--add-to-backend",
dest="add_to_backend",
help="Create a public network to a Ganeti backend."),
metavar="BACKEND_ID",
help="Create a network to a Ganeti backend."),
make_option(
"--remove-from-backend",
dest="remove_from_backend",
help="Remove a public network from a Ganeti backend."),
make_option(
"--floating-ip-pool",
dest="floating_ip_pool",
metavar="True|False",
choices=["True", "False"],
help="Convert network to a floating IP pool. During this"
" conversation the network will be created to all"
" available Ganeti backends."),
metavar="BACKEND_ID",
help="Remove a network from a Ganeti backend."),
)
@transaction.commit_on_success
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError("Please provide a network ID")
network = get_network(args[0])
# Validate subnet
subnet = options["subnet"] or network.subnet
gateway = options["gateway"] or network.gateway
subnet6 = options["subnet6"] or network.subnet6
gateway6 = options["gateway6"] or network.gateway6
networks.validate_network_params(subnet, gateway, subnet6, gateway6)
new_name = options.get("name")
if new_name is not None:
old_name = network.name
network = networks.rename(network, new_name)
self.stdout.write("Renamed network '%s' from '%s' to '%s'.\n" %
(network, old_name, new_name))
# Validate state
state = options.get('state')
if state:
allowed = [x[0] for x in Network.OPER_STATES]
if state not in allowed:
msg = "Invalid state, must be one of %s" % ', '.join(allowed)
raise CommandError(msg)
drained = options.get("drained")
if drained is not None:
drained = parse_bool(drained)
network.drained = drained
network.save()
self.stdout.write("Set network '%s' as drained=%s.\n" %
(network, drained))
new_owner = options.get("userid")
if new_owner is not None:
if "@" in new_owner:
raise CommandError("Invalid owner UUID.")
old_owner = network.userid
network.userid = new_owner
network.save()
msg = "Changed the owner of network '%s' from '%s' to '%s'.\n"
self.stdout.write(msg % (network, old_owner, new_owner))
floating_ip_pool = options["floating_ip_pool"]
if floating_ip_pool is not None:
floating_ip_pool = parse_bool(floating_ip_pool)
options["floating_ip_pool"] = floating_ip_pool
if floating_ip_pool is False and network.floating_ip_pool is True:
if network.floating_ips.filter(deleted=False).exists():
msg = ("Can not make network a non floating IP pool. There are"
" still reserved floating IPs.")
raise CommandError(msg)
elif floating_ip_pool is True:
existing =\
network.backend_networks.filter(operstate="ACTIVE")\
.values_list("backend", flat=True)
for backend in Backend.objects.filter(offline=False)\
.exclude(id__in=existing):
check_link_availability(backend, network)
dhcp = options.get("dhcp")
if dhcp:
options["dhcp"] = parse_bool(dhcp)
drained = options.get("drained")
if drained:
options["drained"] = parse_bool(drained)
fields = ('name', 'userid', 'subnet', 'gateway', 'subnet6', 'gateway6',
'dhcp', 'state', 'link', 'mac_prefix', 'drained',
'floating_ip_pool')
for field in fields:
value = options.get(field, None)
if value is not None:
network.__setattr__(field, value)
network.save()
if floating_ip_pool is False and network.floating_ip_pool is True:
if network.floating_ips.filter(deleted=False).exists():
msg = ("Can not make network a non floating IP pool."
" There are still reserved floating IPs.")
raise CommandError(msg)
network.floating_ip_pool = floating_ip_pool
network.save()
self.stdout.write("Set network '%s' as floating-ip-pool=%s.\n" %
(network, floating_ip_pool))
if floating_ip_pool is True:
for backend in Backend.objects.filter(offline=False):
try:
bnet = network.backend_networks.get(backend=backend)
except BackendNetwork.DoesNotExist:
bnet = network.create_backend_network(backend=backend)
if bnet.operstate != "ACTIVE":
backend_mod.create_network(network, backend,
connect=True)
msg = ("Sent job to create network '%s' in backend"
" '%s'\n" % (network, backend))
self.stdout.write(msg)
add_reserved_ips = options.get('add_reserved_ips')
remove_reserved_ips = options.get('remove_reserved_ips')
......@@ -199,28 +156,17 @@ class Command(BaseCommand):
if remove_reserved_ips:
remove_reserved_ips = remove_reserved_ips.split(",")
for bnetwork in network.backend_networks.all():
for bnetwork in network.backend_networks.filter(offline=False):
with pooled_rapi_client(bnetwork.backend) as c:
c.ModifyNetwork(network=network.backend_id,
add_reserved_ips=add_reserved_ips,
remove_reserved_ips=remove_reserved_ips)
if floating_ip_pool is True:
for backend in Backend.objects.filter(offline=False):
try:
bnet = network.backend_networks.get(backend=backend)
except BackendNetwork.DoesNotExist:
bnet = network.create_backend_network(backend=backend)
if bnet.operstate != "ACTIVE":
create_network(network, backend, connect=True)
msg = "Sent job to create network '%s' in backend '%s'\n"
self.stdout.write(msg % (network, backend))
add_to_backend = options["add_to_backend"]
if add_to_backend is not None:
backend = get_backend(add_to_backend)
network.create_backend_network(backend=backend)
create_network(network, backend, connect=True)
backend_mod.create_network(network, backend, connect=True)
msg = "Sent job to create network '%s' in backend '%s'\n"
self.stdout.write(msg % (network, backend))
......@@ -234,26 +180,6 @@ class Command(BaseCommand):
raise CommandError(msg)
network.action = "DESTROY"
network.save()
delete_network(network, backend, disconnect=True)
backend_mod.delete_network(network, backend, disconnect=True)
msg = "Sent job to delete network '%s' from backend '%s'\n"
self.stdout.write(msg % (network, backend))
def check_link_availability(backend, network):
"""Check if network link is available in backend."""
with pooled_rapi_client(backend) as c:
ganeti_networks = c.GetNetworks(bulk=True)
name = network.backend_id
mode = network.mode
link = network.link
for gnet in ganeti_networks:
if (gnet["name"] != name and
reduce(lambda x, y: x or y,
["(%s, %s)" % (mode, link) in gnet["group_list"]],
False)):
# Ganeti >= 2.7
#(mode, link) in [(m, l) for (_, m, l) in gnet["group_list"]]):
msg = "Can not create network '%s' in backend '%s'. Link '%s'" \
" is already used by network '%s" % \
(network, backend, link, gnet["name"])
raise CommandError(msg)
# Copyright 2012 GRNET S.A. All rights reserved.
# 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
......@@ -33,84 +33,113 @@
from optparse import make_option
from django.db import transaction
from django.core.management.base import BaseCommand, CommandError
from synnefo.management.common import get_vm
from synnefo.management.common import (get_vm, get_flavor, convert_api_faults,
wait_server_task)
from synnefo.webproject.management.utils import parse_bool
from synnefo.logic import servers
from synnefo.db.models import VirtualMachine
ACTIONS = ["start", "stop", "reboot_hard", "reboot_soft"]
class Command(BaseCommand):
args = "<server ID>"
help = "Modify a server"
help = "Modify a server."
option_list = BaseCommand.option_list + (
make_option(
'--name',
dest='name',
metavar='NAME',
help="Set server's name"),
help="Rename server."),
make_option(
'--owner',
dest='owner',
metavar='USER_ID',
help="Set server's owner"),
make_option(
'--state',
dest='state',
metavar='STATE',
help="Set server's state"),
metavar='USER_UUID',
help="Change ownership of server. Value must be a user UUID"),
make_option(
'--set-deleted',
action='store_true',
dest='deleted',
help="Mark a server as deleted"),
"--suspended",
dest="suspended",
default=None,
choices=["True", "False"],
metavar="True|False",
help="Mark a server as suspended/non-suspended."),
make_option(
'--set-undeleted',
action='store_true',
dest='undeleted',
help="Mark a server as not deleted"),
"--flavor",
dest="flavor",
metavar="FLAVOR_ID",
help="Resize a server by modifying its flavor. The new flavor"
" must have the same disk size and disk template."),
make_option(
'--set-suspended',
action='store_true',
dest='suspended',
help="Mark a server as suspended"),
"--action",
dest="action",
choices=ACTIONS,
metavar="|".join(ACTIONS),
help="Perform one of the allowed actions."),
make_option(
'--set-unsuspended',
action='store_true',
dest='unsuspended',
help="Mark a server as not suspended")
"--wait",
dest="wait",
default="True",
choices=["True", "False"],
metavar="True|False",
help="Wait for Ganeti jobs to complete."),
)
@transaction.commit_on_success
@convert_api_faults
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError("Please provide a server ID")
server = get_vm(args[0])
name = options.get('name')
if name is not None:
server.name = name
owner = options.get('owner')
if owner is not None:
server.userid = owner
new_name = options.get("name", None)
if new_name is not None:
old_name = server.name
server = servers.rename(server, new_name)
self.stdout.write("Renamed server '%s' from '%s' to '%s'\n" %
(server, old_name, new_name))
state = options.get('state')
if state is not None:
allowed = [x[0] for x in VirtualMachine.OPER_STATES]
if state not in allowed:
msg = "Invalid state, must be one of %s" % ', '.join(allowed)
raise CommandError(msg)
server.operstate = state
suspended = options.get("suspended", None)
if suspended is not None:
suspended = parse_bool(suspended)
server.suspended = suspended
server.save()
self.stdout.write("Set server '%s' as suspended=%s\n" %
(server, suspended))
if options.get('deleted'):
server.deleted = True
elif options.get('undeleted'):
server.deleted = False
new_owner = options.get('owner')
if new_owner is not None:
if "@" in new_owner:
raise CommandError("Invalid owner UUID.")
old_owner = server.userid
server.userid = new_owner
server.save()
msg = "Changed the owner of server '%s' from '%s' to '%s'.\n"
self.stdout.write(msg % (server, old_owner, new_owner))
if options.get('suspended'):
server.suspended = True
elif options.get('unsuspended'):
server.suspended = False
wait = parse_bool(options["wait"])
new_flavor_id = options.get("flavor")
if new_flavor_id is not None:
new_flavor = get_flavor(new_flavor_id)
old_flavor = server.flavor
msg = "Resizing server '%s' from flavor '%s' to '%s'.\n"
self.stdout.write(msg % (server, old_flavor, new_flavor))
server = servers.resize(server, new_flavor)
wait_server_task(server, wait, stdout=self.stdout)
server.save()
action = options.get("action")
if action is not None:
if action == "start":
server = servers.start(server)
elif action == "stop":
server = servers.stop(server)
elif action == "reboot_hard":
server = servers.reboot(server, reboot_type="HARD")
elif action == "reboot_stof":
server = servers.reboot(server, reboot_type="SOFT")
else:
raise CommandError("Unknown action.")
wait_server_task(server, wait, stdout=self.stdout)
......@@ -34,8 +34,9 @@
from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
from synnefo.management.common import get_vm, convert_api_faults
from synnefo.logic import servers, backend as backend_mod
from synnefo.management.common import (get_vm, convert_api_faults,
wait_server_task)
from synnefo.logic import servers
from snf_django.management.utils import parse_bool
......@@ -63,17 +64,10 @@ class Command(BaseCommand):
self.stdout.write("Trying to remove server '%s' from backend '%s'\n" %
(server.backend_vm_id, server.backend))
servers.destroy(server)
server = servers.destroy(server)
jobID = server.task_job_id
self.stdout.write("Issued OP_INSTANCE_REMOVE with id: %s\n" % jobID)
wait = parse_bool(options["wait"])
if wait:
self.stdout.write("Waiting for job to complete...\n")
client = server.get_client()
status, error = backend_mod.wait_for_job(client, jobID)
if status == "success":
self.stdout.write("Job '%s' completed successfully.\n" % jobID)
else:
self.stdout.write("Job '%s' failed: %s\n" % (jobID, error))
wait_server_task(server, wait, self.stdout)
......@@ -414,8 +414,8 @@ def update_server_name(request, server_id):
vm = util.get_vm(server_id, request.user_uniq, for_update=True,
non_suspended=True)
vm.name = name
vm.save()
servers.rename(vm, new_name=name)
return HttpResponse(status=204)
......
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding unique constraint on 'NetworkInterface', fields ['network', 'ipv4']
db.create_unique('db_networkinterface', ['network_id', 'ipv4'])
def backwards(self, orm):
# Removing unique constraint on 'NetworkInterface', fields ['network', 'ipv4']
db.delete_unique('db_networkinterface', ['network_id', 'ipv4'])
models = {
'db.backend': {
'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'},
'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'disk_templates': ('synnefo.db.fields.SeparatedValuesField', [], {'null': 'True'}),
'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'hypervisor': ('django.db.models.fields.CharField', [], {'default': "'kvm'", 'max_length': '32'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}),
'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'})
},
'db.backendnetwork': {
'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'},
'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'on_delete': 'models.PROTECT', 'to': "orm['db.Backend']"}),
'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'to': "orm['db.Network']"}),
'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}),