Statistics
| Branch: | Tag: | Revision:

root / snf-ganeti-tools / synnefo / ganeti / hook.py @ 45ebfd48

History | View | Annotate | Download (9.5 kB)

1 ff55193e Vangelis Koukis
#!/usr/bin/env python
2 ff55193e Vangelis Koukis
#
3 737b0e28 Vangelis Koukis
# -*- coding: utf-8 -*-
4 737b0e28 Vangelis Koukis
#
5 737b0e28 Vangelis Koukis
# Copyright 2011 GRNET S.A. All rights reserved.
6 737b0e28 Vangelis Koukis
#
7 737b0e28 Vangelis Koukis
# Redistribution and use in source and binary forms, with or
8 737b0e28 Vangelis Koukis
# without modification, are permitted provided that the following
9 737b0e28 Vangelis Koukis
# conditions are met:
10 737b0e28 Vangelis Koukis
#
11 737b0e28 Vangelis Koukis
#   1. Redistributions of source code must retain the above
12 737b0e28 Vangelis Koukis
#      copyright notice, this list of conditions and the following
13 737b0e28 Vangelis Koukis
#      disclaimer.
14 737b0e28 Vangelis Koukis
#
15 737b0e28 Vangelis Koukis
#   2. Redistributions in binary form must reproduce the above
16 737b0e28 Vangelis Koukis
#      copyright notice, this list of conditions and the following
17 737b0e28 Vangelis Koukis
#      disclaimer in the documentation and/or other materials
18 737b0e28 Vangelis Koukis
#      provided with the distribution.
19 737b0e28 Vangelis Koukis
#
20 737b0e28 Vangelis Koukis
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
21 737b0e28 Vangelis Koukis
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 737b0e28 Vangelis Koukis
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 737b0e28 Vangelis Koukis
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
24 737b0e28 Vangelis Koukis
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 737b0e28 Vangelis Koukis
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 737b0e28 Vangelis Koukis
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
27 737b0e28 Vangelis Koukis
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
28 737b0e28 Vangelis Koukis
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 737b0e28 Vangelis Koukis
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30 737b0e28 Vangelis Koukis
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 737b0e28 Vangelis Koukis
# POSSIBILITY OF SUCH DAMAGE.
32 737b0e28 Vangelis Koukis
#
33 737b0e28 Vangelis Koukis
# The views and conclusions contained in the software and
34 737b0e28 Vangelis Koukis
# documentation are those of the authors and should not be
35 737b0e28 Vangelis Koukis
# interpreted as representing official policies, either expressed
36 737b0e28 Vangelis Koukis
# or implied, of GRNET S.A.
37 ff55193e Vangelis Koukis
#
38 ff55193e Vangelis Koukis
"""Ganeti hooks for Synnefo
39 ff55193e Vangelis Koukis

40 ff55193e Vangelis Koukis
These are the individual Ganeti hooks for Synnefo.
41 ff55193e Vangelis Koukis

42 ff55193e Vangelis Koukis
"""
43 ff55193e Vangelis Koukis
44 ff55193e Vangelis Koukis
import sys
45 ff55193e Vangelis Koukis
import os
46 45ebfd48 Vangelis Koukis
import subprocess
47 ff55193e Vangelis Koukis
48 ff55193e Vangelis Koukis
import time
49 ff55193e Vangelis Koukis
import json
50 ff55193e Vangelis Koukis
import socket
51 ff55193e Vangelis Koukis
import logging
52 ff55193e Vangelis Koukis
53 b9eef123 Vangelis Koukis
from amqplib import client_0_8 as amqp
54 ff55193e Vangelis Koukis
55 45ebfd48 Vangelis Koukis
try:
56 45ebfd48 Vangelis Koukis
    conf_dir = os.environ["SYNNEFO_CONFIG_DIR"]
57 45ebfd48 Vangelis Koukis
    import config
58 45ebfd48 Vangelis Koukis
    settings = config.load(conf_dir)
59 45ebfd48 Vangelis Koukis
except KeyError:
60 45ebfd48 Vangelis Koukis
    import synnefo.settings as settings
61 45ebfd48 Vangelis Koukis
62 45ebfd48 Vangelis Koukis
63 45ebfd48 Vangelis Koukis
def mac2eui64(mac, prefixstr):
64 45ebfd48 Vangelis Koukis
    process = subprocess.Popen(["mac2eui64", mac, prefixstr],
65 45ebfd48 Vangelis Koukis
                                stdout=subprocess.PIPE)
66 45ebfd48 Vangelis Koukis
    return process.stdout.read().rstrip()
67 ff55193e Vangelis Koukis
68 ff55193e Vangelis Koukis
def ganeti_net_status(logger, environ):
69 ff55193e Vangelis Koukis
    """Produce notifications of type 'Ganeti-net-status'
70 737b0e28 Vangelis Koukis

71 ff55193e Vangelis Koukis
    Process all GANETI_INSTANCE_NICx_y environment variables,
72 ff55193e Vangelis Koukis
    where x is the NIC index, starting at 0,
73 f533f224 Vangelis Koukis
    and y is one of "MAC", "IP", "BRIDGE".
74 ff55193e Vangelis Koukis

75 ff55193e Vangelis Koukis
    The result is returned as a single notification message
76 f533f224 Vangelis Koukis
    of type 'ganeti-net-status', detailing the NIC configuration
77 ff55193e Vangelis Koukis
    of a Ganeti instance.
78 ff55193e Vangelis Koukis

79 ff55193e Vangelis Koukis
    """
80 ff55193e Vangelis Koukis
    nics = {}
81 ff55193e Vangelis Koukis
82 f533f224 Vangelis Koukis
    key_to_attr = { 'IP': 'ip', 'MAC': 'mac', 'BRIDGE': 'link' }
83 f533f224 Vangelis Koukis
84 ff55193e Vangelis Koukis
    for env in environ.keys():
85 ff55193e Vangelis Koukis
        if env.startswith("GANETI_INSTANCE_NIC"):
86 ff55193e Vangelis Koukis
            s = env.replace("GANETI_INSTANCE_NIC", "").split('_', 1)
87 f533f224 Vangelis Koukis
            if len(s) == 2 and s[0].isdigit() and s[1] in ('MAC', 'IP', 'BRIDGE'):
88 ff55193e Vangelis Koukis
                index = int(s[0])
89 f533f224 Vangelis Koukis
                key = key_to_attr[s[1]]
90 ff55193e Vangelis Koukis
91 ff55193e Vangelis Koukis
                if nics.has_key(index):
92 ff55193e Vangelis Koukis
                    nics[index][key] = environ[env]
93 ff55193e Vangelis Koukis
                else:
94 ff55193e Vangelis Koukis
                    nics[index] = { key: environ[env] }
95 ff55193e Vangelis Koukis
96 746c6bf4 Vangelis Koukis
                # IPv6 support:
97 746c6bf4 Vangelis Koukis
                #
98 746c6bf4 Vangelis Koukis
                # The IPv6 of NIC with index 0 [the public NIC]
99 746c6bf4 Vangelis Koukis
                # is derived using an EUI64 scheme.
100 746c6bf4 Vangelis Koukis
                if index == 0 and key == 'mac':
101 746c6bf4 Vangelis Koukis
                    nics[0]['ipv6'] = mac2eui64(nics[0]['mac'],
102 746c6bf4 Vangelis Koukis
                                                settings.PUBLIC_IPV6_PREFIX)
103 746c6bf4 Vangelis Koukis
104 eab0602e Vangelis Koukis
    # Amend notification with firewall settings
105 eab0602e Vangelis Koukis
    tags = environ.get('GANETI_INSTANCE_TAGS', '')
106 eab0602e Vangelis Koukis
    for tag in tags.split(' '):
107 eab0602e Vangelis Koukis
        t = tag.split(':')
108 eab0602e Vangelis Koukis
        if t[0:2] == ['synnefo', 'network']:
109 eab0602e Vangelis Koukis
            if len(t) != 4:
110 eab0602e Vangelis Koukis
                logger.error("Malformed synnefo tag %s", tag)
111 eab0602e Vangelis Koukis
                continue
112 eab0602e Vangelis Koukis
            try:
113 eab0602e Vangelis Koukis
                index = int(t[2])
114 eab0602e Vangelis Koukis
                nics[index]['firewall'] = t[3]
115 eab0602e Vangelis Koukis
            except ValueError:
116 eab0602e Vangelis Koukis
                logger.error("Malformed synnefo tag %s", tag)
117 eab0602e Vangelis Koukis
            except KeyError:
118 eab0602e Vangelis Koukis
                logger.error("Found tag %s for non-existent NIC %d",
119 eab0602e Vangelis Koukis
                             tag, index)
120 eab0602e Vangelis Koukis
121 ff55193e Vangelis Koukis
    # Verify our findings are consistent with the Ganeti environment
122 ff55193e Vangelis Koukis
    indexes = list(nics.keys())
123 ff55193e Vangelis Koukis
    ganeti_nic_count = int(environ['GANETI_INSTANCE_NIC_COUNT'])
124 ff55193e Vangelis Koukis
    if len(indexes) != ganeti_nic_count:
125 ff55193e Vangelis Koukis
        logger.error("I have %d NICs, Ganeti says number of NICs is %d",
126 ff55193e Vangelis Koukis
            len(indexes), ganeti_nic_count)
127 ff55193e Vangelis Koukis
        raise Exception("Inconsistent number of NICs in Ganeti environment")
128 ff55193e Vangelis Koukis
129 ff55193e Vangelis Koukis
    if indexes != range(0, len(indexes)):
130 ff55193e Vangelis Koukis
        logger.error("Ganeti NIC indexes are not consecutive starting at zero.");
131 ff55193e Vangelis Koukis
        logger.error("NIC indexes are: %s. Environment is: %s", indexes, environ)
132 ff55193e Vangelis Koukis
        raise Exception("Unexpected inconsistency in the Ganeti environment")
133 ff55193e Vangelis Koukis
134 ff55193e Vangelis Koukis
    # Construct the notification
135 ff55193e Vangelis Koukis
    instance = environ['GANETI_INSTANCE_NAME']
136 ff55193e Vangelis Koukis
137 ff55193e Vangelis Koukis
    nics_list = []
138 ff55193e Vangelis Koukis
    for i in indexes:
139 ff55193e Vangelis Koukis
        nics_list.append(nics[i])
140 ff55193e Vangelis Koukis
141 ff55193e Vangelis Koukis
    msg = {
142 ff55193e Vangelis Koukis
        "type": "ganeti-net-status",
143 ff55193e Vangelis Koukis
        "instance": instance,
144 ff55193e Vangelis Koukis
        "nics": nics_list
145 ff55193e Vangelis Koukis
    }
146 ff55193e Vangelis Koukis
147 ff55193e Vangelis Koukis
    return msg
148 ff55193e Vangelis Koukis
149 b9eef123 Vangelis Koukis
150 b9eef123 Vangelis Koukis
class GanetiHook():
151 b9eef123 Vangelis Koukis
    def __init__(self, logger, environ, instance, prefix):
152 b9eef123 Vangelis Koukis
        self.logger = logger
153 b9eef123 Vangelis Koukis
        self.environ = environ
154 b9eef123 Vangelis Koukis
        self.instance = instance
155 b9eef123 Vangelis Koukis
        self.prefix = prefix
156 b9eef123 Vangelis Koukis
157 b9eef123 Vangelis Koukis
    def on_master(self):
158 b9eef123 Vangelis Koukis
        """Return True if running on the Ganeti master"""
159 b9eef123 Vangelis Koukis
        return socket.getfqdn() == self.environ['GANETI_MASTER']
160 b9eef123 Vangelis Koukis
161 b9eef123 Vangelis Koukis
    def publish_msgs(self, msgs):
162 b9eef123 Vangelis Koukis
        for (msgtype, msg) in msgs:
163 b9eef123 Vangelis Koukis
            routekey = "ganeti.%s.event.%s" % (self.prefix, msgtype)
164 b9eef123 Vangelis Koukis
            self.logger.debug("Pushing message to RabbitMQ: %s (key = %s)",
165 b9eef123 Vangelis Koukis
                json.dumps(msg), routekey)
166 b9eef123 Vangelis Koukis
            msg = amqp.Message(json.dumps(msg))
167 b9eef123 Vangelis Koukis
            msg.properties["delivery_mode"] = 2  # Persistent
168 b9eef123 Vangelis Koukis
169 b9eef123 Vangelis Koukis
            # Retry up to five times to open a channel to RabbitMQ.
170 b9eef123 Vangelis Koukis
            # The hook needs to abort if this count is exceeded, because it
171 b9eef123 Vangelis Koukis
            # runs synchronously with VM creation inside Ganeti, and may only
172 b9eef123 Vangelis Koukis
            # run for a finite amount of time.
173 b9eef123 Vangelis Koukis
            #
174 b9eef123 Vangelis Koukis
            # FIXME: We need a reconciliation mechanism between the DB and
175 b9eef123 Vangelis Koukis
            #        Ganeti, for cases exactly like this.
176 b9eef123 Vangelis Koukis
            conn = None
177 b9eef123 Vangelis Koukis
            sent = False
178 b9eef123 Vangelis Koukis
            retry = 0
179 b9eef123 Vangelis Koukis
            while not sent and retry < 5:
180 b9eef123 Vangelis Koukis
                self.logger.debug("Attempting to publish to RabbitMQ at %s",
181 b9eef123 Vangelis Koukis
                    settings.RABBIT_HOST)
182 b9eef123 Vangelis Koukis
                try:
183 b9eef123 Vangelis Koukis
                    if not conn:
184 b9eef123 Vangelis Koukis
                        conn = amqp.Connection(host=settings.RABBIT_HOST,
185 b9eef123 Vangelis Koukis
                            userid=settings.RABBIT_USERNAME,
186 b9eef123 Vangelis Koukis
                            password=settings.RABBIT_PASSWORD,
187 b9eef123 Vangelis Koukis
                            virtual_host=settings.RABBIT_VHOST)
188 b9eef123 Vangelis Koukis
                        chann = conn.channel()
189 b9eef123 Vangelis Koukis
                        self.logger.debug("Successfully connected to RabbitMQ at %s",
190 b9eef123 Vangelis Koukis
                            settings.RABBIT_HOST)
191 b9eef123 Vangelis Koukis
192 b9eef123 Vangelis Koukis
                    chann.basic_publish(msg,
193 b9eef123 Vangelis Koukis
                        exchange=settings.EXCHANGE_GANETI,
194 b9eef123 Vangelis Koukis
                        routing_key=routekey)
195 b9eef123 Vangelis Koukis
                    sent = True
196 b9eef123 Vangelis Koukis
                    self.logger.debug("Successfully sent message to RabbitMQ")
197 b9eef123 Vangelis Koukis
                except socket.error:
198 b9eef123 Vangelis Koukis
                    conn = False
199 b9eef123 Vangelis Koukis
                    retry += 1
200 b9eef123 Vangelis Koukis
                    self.logger.exception("Publish to RabbitMQ failed, retry=%d in 1s",
201 b9eef123 Vangelis Koukis
                        retry)
202 b9eef123 Vangelis Koukis
                    time.sleep(1)
203 b9eef123 Vangelis Koukis
204 b9eef123 Vangelis Koukis
            if not sent:
205 b9eef123 Vangelis Koukis
                raise Exception("Publish to RabbitMQ failed after %d tries, aborting" % retry)
206 b9eef123 Vangelis Koukis
207 b9eef123 Vangelis Koukis
208 b9eef123 Vangelis Koukis
class PostStartHook(GanetiHook):
209 b9eef123 Vangelis Koukis
    """Post-instance-startup Ganeti Hook.
210 b9eef123 Vangelis Koukis

211 b9eef123 Vangelis Koukis
    Produce notifications to the rest of the Synnefo
212 b9eef123 Vangelis Koukis
    infrastructure in the post-instance-start phase of Ganeti.
213 b9eef123 Vangelis Koukis

214 ff55193e Vangelis Koukis
    Currently, this list only contains a single message,
215 ff55193e Vangelis Koukis
    detailing the net configuration of an instance.
216 ff55193e Vangelis Koukis

217 b9eef123 Vangelis Koukis
    This hook only runs on the Ganeti master.
218 b9eef123 Vangelis Koukis

219 ff55193e Vangelis Koukis
    """
220 b9eef123 Vangelis Koukis
    def run(self):
221 b9eef123 Vangelis Koukis
        if self.on_master():
222 b9eef123 Vangelis Koukis
            notifs = []
223 b9eef123 Vangelis Koukis
            notifs.append(("net", ganeti_net_status(self.logger, self.environ)))
224 737b0e28 Vangelis Koukis
225 b9eef123 Vangelis Koukis
            self.publish_msgs(notifs)
226 b9eef123 Vangelis Koukis
227 7ca9e930 Vangelis Koukis
        return 0
228 ff55193e Vangelis Koukis
229 ff55193e Vangelis Koukis
230 b9eef123 Vangelis Koukis
class PostStopHook(GanetiHook):
231 b9eef123 Vangelis Koukis
    def run(self):
232 b9eef123 Vangelis Koukis
        return 0
233 ff55193e Vangelis Koukis
234 45ebfd48 Vangelis Koukis
235 45ebfd48 Vangelis Koukis
def main():
236 45ebfd48 Vangelis Koukis
    logging.basicConfig(level=logging.DEBUG)
237 45ebfd48 Vangelis Koukis
    logger = logging.getLogger("synnefo.ganeti")
238 45ebfd48 Vangelis Koukis
239 45ebfd48 Vangelis Koukis
    try:
240 45ebfd48 Vangelis Koukis
        instance = os.environ['GANETI_INSTANCE_NAME']
241 45ebfd48 Vangelis Koukis
        op = os.environ['GANETI_HOOKS_PATH']
242 45ebfd48 Vangelis Koukis
        phase = os.environ['GANETI_HOOKS_PHASE']
243 45ebfd48 Vangelis Koukis
    except KeyError:
244 45ebfd48 Vangelis Koukis
        raise Exception("Environment missing one of: " \
245 45ebfd48 Vangelis Koukis
            "GANETI_INSTANCE_NAME, GANETI_HOOKS_PATH, GANETI_HOOKS_PHASE")
246 45ebfd48 Vangelis Koukis
247 45ebfd48 Vangelis Koukis
    prefix = instance.split('-')[0]
248 45ebfd48 Vangelis Koukis
249 45ebfd48 Vangelis Koukis
    # FIXME: The hooks should only run for Synnefo instances.
250 45ebfd48 Vangelis Koukis
    # Uncomment the following lines for a shared Ganeti deployment.
251 45ebfd48 Vangelis Koukis
    # Currently, the following code is commented out because multiple
252 45ebfd48 Vangelis Koukis
    # backend prefixes are used in the same Ganeti installation during development.
253 45ebfd48 Vangelis Koukis
    #if not instance.startswith(settings.BACKEND_PREFIX_ID):
254 45ebfd48 Vangelis Koukis
    #    logger.warning("Ignoring non-Synnefo instance %s", instance)
255 45ebfd48 Vangelis Koukis
    #    return 0
256 45ebfd48 Vangelis Koukis
257 45ebfd48 Vangelis Koukis
    hooks = {
258 45ebfd48 Vangelis Koukis
        ("instance-add", "post"): PostStartHook,
259 45ebfd48 Vangelis Koukis
        ("instance-start", "post"): PostStartHook,
260 45ebfd48 Vangelis Koukis
        ("instance-reboot", "post"): PostStartHook,
261 45ebfd48 Vangelis Koukis
        ("instance-stop", "post"): PostStopHook,
262 45ebfd48 Vangelis Koukis
        ("instance-modify", "post"): PostStartHook
263 45ebfd48 Vangelis Koukis
    }
264 45ebfd48 Vangelis Koukis
265 45ebfd48 Vangelis Koukis
    try:
266 45ebfd48 Vangelis Koukis
        hook = hooks[(op, phase)](logger, os.environ, instance, prefix)
267 45ebfd48 Vangelis Koukis
    except KeyError:
268 45ebfd48 Vangelis Koukis
        raise Exception("No hook found for operation = '%s', phase = '%s'" % (op, phase))
269 45ebfd48 Vangelis Koukis
    return hook.run()
270 45ebfd48 Vangelis Koukis
271 45ebfd48 Vangelis Koukis
if __name__ == "__main__":
272 45ebfd48 Vangelis Koukis
    sys.exit(main())