Revision 88d998b9

b/snf-cyclades-app/synnefo/tools/add_unique_name_to_nics.py
1
#!/usr/bin/env python
2
"""Tool to update Ganeti instances:
3

  
4
* add unique name to the NICs of all Ganeti instances
5
* rename all instance tags related with network firewall profiles to include
6
  the unique name of the corresponding NIC.
7

  
8
The name for each NIC is based on the PK of the NIC in Cyclades DB.
9
"""
10

  
11
FIREWALL_TAGS_PREFIX = "synnefo:network:"
12
FIREWALL_TAGS = {"ENABLED": "synnefo:network:%s:protected",
13
                 "DISABLED": "synnefo:network:%s:unprotected",
14
                 "PROTECTED": "synnefo:network:%s:limited"}
15

  
16
# Gevent patching
17
import gevent
18
from gevent import monkey
19
monkey.patch_all()
20

  
21
import sys
22
import subprocess
23
from optparse import OptionParser, TitledHelpFormatter
24

  
25
# Configure Django env
26
from synnefo import settings
27
from django.core.management import setup_environ
28
setup_environ(settings)
29

  
30
from django.db import close_connection
31
from synnefo.db.models import Backend, pooled_rapi_client
32
from synnefo.management.common import get_backend
33

  
34
import logging
35
logger = logging.getLogger("migrate_nics")
36
handler = logging.StreamHandler()
37

  
38
formatter = logging.Formatter("[%(levelname)s] %(message)s")
39
handler.setFormatter(formatter)
40
logger.setLevel(logging.DEBUG)
41
logger.addHandler(handler)
42
logger.propagate = False
43

  
44
DESCRIPTION = """\
45
Tool to update all Ganeti instances in order to add a unique name to NICs of
46
all instances and rename the instance firewall tags to include the NIC name.
47
"""
48

  
49

  
50
def main():
51
    parser = OptionParser(description=DESCRIPTION,
52
                          formatter=TitledHelpFormatter())
53
    parser.add_option("--backend-id", dest="backend_id",
54
                      help="Update instances only of this Ganeti backend."),
55
    parser.add_option("--dry-run", dest="dry_run", default=False,
56
                      action="store_true",
57
                      help="Do not send any jobs to Ganeti backend.")
58
    parser.add_option("--ganeti-dry-run", dest="ganeti_dry_run", default=False,
59
                      action="store_true",
60
                      help="Pass --dry-run option to Ganeti jobs.")
61
    parser.add_option("--parallel", dest="parallel", default=False,
62
                      action="store_true",
63
                      help="Use a seperate process for each backend.")
64
    parser.add_option("-d", "--debug", dest="debug", default=False,
65
                      action="store_true",
66
                      help="Display debug information.")
67
    options, args = parser.parse_args()
68

  
69
    if options.backend_id:
70
        backends = [get_backend(options.backend_id)]
71
    else:
72
        if Backend.objects.filter(offline=True).exists():
73
            msg = "Can not update intances. An 'offline' backend exists."
74
            raise Exception(msg)
75
        backends = Backend.objects.all()
76

  
77
    if options.debug:
78
        logger.setLevel(logging.DEBUG)
79

  
80
    if len(backends) > 1 and options.parallel:
81
        cmd = sys.argv
82
        processes = []
83
        for backend in backends:
84
            p = subprocess.Popen(cmd + ["--backend-id=%s" % backend.id])
85
            processes.append(p)
86
        for p in processes:
87
            p.wait()
88
        return
89
    else:
90
        [upgrade_backend(b, options.dry_run, options.ganeti_dry_run)
91
         for b in backends]
92
    return
93

  
94

  
95
def upgrade_backend(backend, dry_run, ganeti_dry_run):
96
    jobs = []
97
    instances_ids = get_instances_with_anonymous_nics(backend)
98
    for vm in backend.virtual_machines.filter(id__in=instances_ids):
99
        jobs.append(gevent.spawn(upgrade_vm, vm, dry_run, ganeti_dry_run))
100

  
101
    if jobs:
102
        for job_chunk in [jobs[x:x+25] for x in range(0, len(jobs), 25)]:
103
            gevent.joinall(jobs)
104
    else:
105
        logger.info("No anonymous NICs in backend '%s'. Nothing to do!",
106
                    backend.clustername)
107
    return
108

  
109

  
110
def get_instances_with_anonymous_nics(backend):
111
    """Get all Ganeti instances that have NICs without names."""
112
    with pooled_rapi_client(backend) as rc:
113
        instances = rc.GetInstances(bulk=True)
114
    # Filter snf- instances
115
    instances = filter(lambda i:
116
                       i["name"].startswith(settings.BACKEND_PREFIX_ID),
117
                       instances)
118
    # Filter instances with anonymous NICs
119
    instances = filter(lambda i: None in i["nic.names"], instances)
120
    # Get IDs of those instances
121
    instances_ids = map(lambda i:
122
                        i["name"].replace(settings.BACKEND_PREFIX_ID, "", 1),
123
                        instances)
124
    return instances_ids
125

  
126

  
127
def upgrade_vm(vm, dry_run, ganeti_dry_run):
128
    """Add names to Ganeti NICs and update firewall Tags."""
129
    logger.info("Updating NICs of instance %s" % vm.backend_vm_id)
130
    index_to_uuid = {}
131
    new_tags = []
132
    # Compute new NICs names and firewall tags
133
    for nic in vm.nics.all():
134
        if nic.index is None:
135
            msg = ("Cannot update NIC '%s'. The index of the NIC is unknown."
136
                   " Please run snf-manage reconcile-servers --fix-all and"
137
                   " retry!")
138
            logger.critical(msg)
139
            continue
140
        uuid = nic.backend_uuid
141
        # Map index -> UUID
142
        index_to_uuid[nic.index] = uuid
143

  
144
        # New firewall TAG with UUID
145
        firewall_profile = nic.firewall_profile
146
        if firewall_profile and firewall_profile != "DISABLED":
147
            firewall_tag = FIREWALL_TAGS[nic.firewall_profile] % uuid
148
            new_tags.append(firewall_tag)
149

  
150
    renamed_nics = [("modify", index, {"name": name})
151
                    for index, name in index_to_uuid.items()]
152

  
153
    instance = vm.backend_vm_id
154
    with pooled_rapi_client(vm) as rc:
155
        # Delete old Tags
156
        tags = rc.GetInstanceTags(instance)
157
        delete_tags = [t for t in tags if t.startswith(FIREWALL_TAGS_PREFIX)]
158
        if delete_tags:
159
            logger.debug("Deleting tags '%s' from instance '%s'",
160
                         delete_tags, vm.backend_vm_id)
161
            if not dry_run:
162
                rc.DeleteInstanceTags(instance, delete_tags,
163
                                      dry_run=ganeti_dry_run)
164

  
165
        # Add new Tags
166
        if new_tags:
167
            logger.debug("Adding new tags '%s' to instance '%s'",
168
                         new_tags, vm.backend_vm_id)
169
            if not dry_run:
170
                rc.AddInstanceTags(instance, new_tags, dry_run=ganeti_dry_run)
171

  
172
        # Add names to NICs
173
        logger.debug("Modifying NICs of instance '%s'. New NICs: '%s'",
174
                     vm.backend_vm_id, renamed_nics)
175
        if not dry_run:
176
            rc.ModifyInstance(vm.backend_vm_id,
177
                              nics=renamed_nics, dry_run=ganeti_dry_run)
178
    close_connection()
179

  
180

  
181
if __name__ == "__main__":
182
    main()
183
    sys.exit(0)

Also available in: Unified diff