Commit df284363 authored by Dimitris Aragiorgis's avatar Dimitris Aragiorgis

Huuuuge snf-deploy refactor

Introduce the concept of Roles and Components.

Roles are ns, mq, db, nfs, astakos, cyclades, pithos, cms, stats,
ganeti, master, and client.

Each role consists of various SynnefoComponents which  should define:

* commands to check installation prerequisites (check)
* packages to install (REQUIRED_PACKAGES)
* commands to prepare installation (prepare)
* configuration templates (configure)
* reload commands (restart)
* initialization commands (initialize)
* test commands (test)

SynnefoComponents are unaware of fabric environment. They
get initialized with a Host object and a Env object.

Host includes all the necessary info for the installation node
(ip, hostname, alias, fqdn..) and Env includes all the configuration info
(created after parsing config files)

After defining components, fabfile should just do the following:

@roles("somerole")
def setup_somerole_role():
  SetupSynnefoRole("SomeRole")

In case you want to run a component's specific method just run

RunComponentMethod(SomeComponent, "method_name", *args, **kwargs)

During a role setup you might have to retrieve info from other
components already installed (user token, backend id, etc.) Use
'execute' fabric method and fill env object with required info.

Make deployment re-entrant.

Check if specific component is already installed and if yes then skip it.

Currently this is done on node/component level. The component's
status on a target node is either ok or nothing.

Introduce conflicting components.

Two components might not be able/or should not coexist in the same
node, e.g. NFS and Mount. These conflicts are defined in CONFLICTS
dict in roles.py.

Sync ci and deploy conf files.

ci: Change node's password in nodes.conf

Always enable CSRF. Install CMS only if it resides on different
node than pithos, cyclades and astakos.

Add copyright headers.
Signed-off-by: default avatarDimitris Aragiorgis <dimara@grnet.gr>
parent 66fdbb73
[ganeti1]
cluster_nodes = node1
cluster_nodes =
master_node = node1
cluster_netdev = eth0
......
......@@ -21,8 +21,13 @@ node1 = 192.168.0.1
node1 = 52:54:00:00:00:01
# node2 = 52:54:00:00:00:02
[passwords]
node1 = 12345
# node2 = 67890
[info]
# Here we define which nodes from the predefined ones to use
# comma separated node names e.g. node1,node2
nodes = node1
# login credentials for the nodes
......
[debian]
rabbitmq-server = squeeze-backports
gunicorn = squeeze-backports
qemu-kvm = squeeze-backports
qemu = squeeze-backports
python-gevent = squeeze-backports
apache2 =
postgresql =
python-psycopg2 =
python-argparse =
nfs-kernel-server = squeeze-backports
nfs-common = squeeze-backports
bind9 =
vlan =
vlan =
lvm2 =
curl =
memcached =
python-memcache =
bridge-utils =
python-progress =
ganeti-instance-debootstrap =
python-django-south = squeeze-backports
drbd8-utils =
[synnefo]
snf-astakos-app = squeeze
snf-common = squeeze
snf-cyclades-app = squeeze
snf-cyclades-gtools = squeeze
snf-django-lib = squeeze
python-astakosclient = squeeze
snf-branding = squeeze
snf-webproject = squeeze
snf-pithos-app = squeeze
snf-pithos-backend = squeeze
snf-tools = squeeze
[ganeti]
snf-ganeti = 2.6.2+ippool11+hotplug5+extstorage3+rbdfix1+kvmfix2+nolvm+netxen-1~squeeze
ganeti-htools = 2.6.2+ippool11+hotplug5+extstorage3+rbdfix1+kvmfix2+nolvm+netxen-1~squeeze
[other]
snf-cloudcms = squeeze
snf-vncauthproxy = squeeze
snf-pithos-webclient = squeeze
snf-image = squeeze
snf-network = squeeze
python-objpool = squeeze
nfdhcpd = squeeze
kamaki = squeeze
python-bitarray = squeeze-backports
nfqueue-bindings-python = 0.3+physindev-1
......@@ -23,6 +23,7 @@ ns = node1
client = node1
router = node1
stats = node1
nfs = node1
[synnefo]
......
......@@ -24,6 +24,7 @@ python-django-south =
python-django =
drbd8-utils =
collectd =
dnsutils =
[synnefo]
......@@ -45,11 +46,12 @@ snf-stats-app = wheezy
snf-ganeti = wheezy
ganeti-htools = wheezy
ganeti-haskell = wheezy
ganeti2 = wheezy
[other]
snf-cloudcms = wheezy
snf-vncauthproxy = unstable
snf-vncauthproxy = wheezy
snf-pithos-webclient = wheezy
snf-image = wheezy
snf-network = wheezy
......
......@@ -893,6 +893,7 @@ class SynnefoCI(object):
self.logger.debug("Change password in nodes.conf file")
cmd = """
sed -i 's/^password =.*/password = {0}/' /etc/snf-deploy/nodes.conf
sed -i 's/12345/{0}/' /etc/snf-deploy/nodes.conf
""".format(fabric.env.password)
_run(cmd, False)
......
Copyright (C) 2010, 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.
[ganeti1]
cluster_nodes = node1
cluster_nodes =
master_node = node1
cluster_netdev = eth0
......@@ -8,8 +8,8 @@ cluster_ip = 192.168.0.13
vg = autovg
synnefo_public_network_subnet = 10.0.1.0/24
synnefo_public_network_gateway = 10.0.1.1
synnefo_public_network_subnet = 10.2.1.0/24
synnefo_public_network_gateway = 10.2.1.1
synnefo_public_network_type = CUSTOM
image_dir = /srv/okeanos
......
......@@ -4,7 +4,7 @@
domain = synnefo.live
[os]
node1 = squeeze
node1 = wheezy
# node2 = wheezy
[hostnames]
......@@ -21,8 +21,13 @@ node1 = 192.168.0.1
node1 = 52:54:00:00:00:01
# node2 = 52:54:00:00:00:02
[passwords]
node1 = 12345
# node2 = 67890
[info]
# Here we define which nodes from the predefined ones to use
# comma separated node names e.g. node1,node2
nodes = node1
# login credentials for the nodes
......
[debian]
rabbitmq-server = squeeze-backports
gunicorn = squeeze-backports
qemu-kvm = squeeze-backports
qemu = squeeze-backports
python-gevent = squeeze-backports
apache2 =
postgresql =
python-psycopg2 =
python-argparse =
nfs-kernel-server = squeeze-backports
nfs-common = squeeze-backports
bind9 =
vlan =
vlan =
lvm2 =
curl =
memcached =
python-memcache =
bridge-utils =
python-progress =
ganeti-instance-debootstrap =
python-django-south = squeeze-backports
python-django = squeeze-backports
drbd8-utils =
[synnefo]
snf-astakos-app = squeeze
snf-common = squeeze
snf-cyclades-app = squeeze
snf-cyclades-gtools = squeeze
snf-django-lib = squeeze
python-astakosclient = squeeze
snf-branding = squeeze
snf-webproject = squeeze
snf-pithos-app = squeeze
snf-pithos-backend = squeeze
snf-tools = squeeze
[ganeti]
snf-ganeti = 2.6.2+ippool11+hotplug5+extstorage3+rbdfix1+kvmfix2+nolvm+netxen-1~squeeze
ganeti-htools = 2.6.2+ippool11+hotplug5+extstorage3+rbdfix1+kvmfix2+nolvm+netxen-1~squeeze
[other]
snf-cloudcms = squeeze
snf-vncauthproxy = squeeze
snf-pithos-webclient = squeeze
snf-image = squeeze
snf-network = squeeze
python-objpool = squeeze
nfdhcpd = squeeze
kamaki = squeeze
python-bitarray = squeeze-backports
nfqueue-bindings-python = 0.3+physindev-1
......@@ -23,6 +23,7 @@ ns = node1
client = node1
router = node1
stats = node1
nfs = node1
[synnefo]
......
......@@ -24,6 +24,7 @@ python-django-south =
python-django =
drbd8-utils =
collectd =
dnsutils =
[synnefo]
......@@ -45,6 +46,8 @@ snf-stats-app = wheezy
snf-ganeti = wheezy
ganeti-htools = wheezy
ganeti-haskell = wheezy
ganeti2 = wheezy
[other]
snf-cloudcms = wheezy
......
......@@ -6,12 +6,37 @@
// organization
//include "/etc/bind/zones.rfc1918";
include "/etc/bind/ddns.key";
// all synnefo components share the same domain/zone
zone "%DOMAIN%" in {
type master;
notify no;
file "/etc/bind/zones/%DOMAIN%";
allow-update { key DDNS_UPDATE; };
};
# domain/zone for the VMs
zone "vm.%DOMAIN%" in {
type master;
notify no;
file "/etc/bind/zones/vm.%DOMAIN%";
allow-update { key DDNS_UPDATE; };
};
// reverse dns zone for all IPs
zone "in-addr.arpa" in {
type master;
notify no;
file "/etc/bind/rev/synnefo.in-addr.arpa.zone";
allow-update { key DDNS_UPDATE; };
};
// v6 reverse dns zone for all IPs
zone "ip6.arpa" in {
type master;
notify no;
file "/etc/bind/rev/synnefo.ip6.arpa.zone";
allow-update { key DDNS_UPDATE; };
};
$TTL 86400
$ORIGIN ip6.arpa.
@ IN SOA ns.%DOMAIN%. admin.%DOMAIN%. (
2012070900; the Serial Number
172800; the Refresh Rate
7200; the Retry Time
604800; the Expiration Time
3600) ; the Minimum Time
@ IN NS ns.%DOMAIN%.
$ORIGIN .
$TTL 86400 ; 1 day
ip6.arpa IN SOA ns.vm.qa.live. admin.vm.qa.live. (
2012071070 ; serial
172800 ; refresh (2 days)
7200 ; retry (2 hours)
604800 ; expire (1 week)
3600 ; minimum (1 hour)
)
NS ns.vm.qa.live.
$TTL 14400
$origin vm.%DOMAIN%.
@ IN SOA ns.vm.%DOMAIN%. admin.vm.%DOMAIN%. (
2012111903; the Serial Number
172800; the Refresh Rate
7200; the Retry Time
604800; the Expiration Time
3600; the Minimum Time
)
@ IN NS ns.vm.%DOMAIN%.
@ IN A %NS_NODE_IP%
ns IN A %NS_NODE_IP%
......@@ -8,7 +8,6 @@
# IMAGE_DIR: directory location for disk images
# IMAGE_DIR="/var/lib/snf-image"
IMAGE_DIR=%IMAGE_DIR%
# IMAGE_DEBUG: turn on debugging output for the scripts
# IMAGE_DEBUG=no
......@@ -43,7 +42,6 @@ IMAGE_DIR=%IMAGE_DIR%
# for days.
# HELPER_SOFT_TIMEOUT="20"
# HELPER_HARD_TIMEOUT="5"
HELPER_SOFT_TIMEOUT=100
# HELPER_USER: For security reasons, it is recommended that the helper VM
# runs as an unprivileged user. KVM drops root privileges and runs as
......@@ -60,17 +58,14 @@ HELPER_SOFT_TIMEOUT=100
# PITHOS_DB: Pithos database in SQLAlchemy format
# PITHOS_DB="sqlite:////var/lib/pithos/backend.db"
PITHOS_DB=postgresql://%SYNNEFO_USER%:%SYNNEFO_DB_PASSWD%@%DB_NODE%:5432/snf_pithos
# PITHOS_DATA: Directory where pithos data are hosted
# PITHOS_DATA="/var/lib/pithos/data"
PITHOS_DATA=%PITHOS_DIR%/data
# PROGRESS_MONITOR: External program that monitors the progress of the image
# deployment. The snf-image monitor messages will be redirected to the standard
# input of this program.
# PROGRESS_MONITOR=""
PROGRESS_MONITOR=snf-progress-monitor
# UNATTEND: This variables overwrites the unattend.xml file used when deploying
# a windows image. snf-image-helper will use its own unattend.xml file if this
......@@ -87,4 +82,9 @@ PROGRESS_MONITOR=snf-progress-monitor
# INSTALL_MBR="install-mbr"
# TIMELIMIT="timelimit"
# CURL="curl"
IMAGE_DIR=%IMAGE_DIR%
HELPER_SOFT_TIMEOUT=100
PITHOS_DB=postgresql://%SYNNEFO_USER%:%SYNNEFO_DB_PASSWD%@%DB_NODE%:5432/snf_pithos
PITHOS_DATA=%PITHOS_DIR%/data
PROGRESS_MONITOR=snf-progress-monitor
CURL="curl -k"
MAC_MASK=ff:ff:f0:00:00:00
TAP_CONSTANT_MAC=cc:47:52:4e:45:54 # GRNET in hex :-)
MAC2EUI64=/usr/bin/mac2eui64
NFDHCPD_STATE_DIR=/var/lib/nfdhcpd
GANETI_NIC_DIR=/var/run/ganeti/xen-hypervisor/nic
MAC_FILTERED_TAG=private-filtered
NFDHCPD_TAG=nfdhcpd
IP_LESS_ROUTED_TAG=ip-less-routed
MASQ_TAG=masq
PUBLIC_TAG=public
DNS_TAG=public
# Default options for runlocked helper script (uncomment to modify)
#RUNLOCKED_OPTS="--id 10001 --retry-sec 0.5"
# NS options needed by nsupdate
# A proper bind configuration is a prerequisite
# Please see: https://wiki.debian.org/DDNS
# If one of the following vars are not set dnshook wont do a thing
# Name server IP/FQDN
SERVER=%SERVER%
# zone for the vms
FZONE=vm.%DOMAIN%
# keyfile path to pass to nsupdate with -k option
# see man page for more info
KEYFILE=%KEYFILE%
/srv/ganeti/file-storage
/srv/ganeti/shared-file-storage
......@@ -8,7 +8,7 @@ CONFIG = {
'group': 'www-data',
'args': (
'--bind=127.0.0.1:8080',
'--workers=8',
'--workers=6',
'--worker-class=gevent',
# '--worker-class=sync',
'--log-level=debug',
......
#!/bin/bash
brctl addbr %COMMON_BRIDGE%
ip link set %COMMON_BRIDGE% up
iptables -t mangle -A PREROUTING -i %COMMON_BRIDGE% -p udp -m udp --dport 67 -j NFQUEUE --queue-num 42
if [ %ROUTER_IP% == %NODE_IP% ]; then
iptables -t nat -A POSTROUTING -o %PUBLIC_IFACE% -s %SUBNET% -j MASQUERADE
echo 1 > /proc/sys/net/ipv4/ip_forward
ip addr add %GATEWAY% dev %COMMON_BRIDGE%
ip route add %SUBNET% dev %COMMON_BRIDGE% src %GATEWAY%
fi
exit 0
# This has been generated automatically by snf-deploy, at %DATE%
# The immutable bit (+i attribute) has been used to avoid it being
# overwritten by software such as NetworkManager or resolvconf.
# Use lsattr/chattr to view or modify its file attributes.
domain %DOMAIN%
search %DOMAIN%
nameserver %NS_NODE_IP%
......@@ -2,7 +2,7 @@ MAX_CIDR_BLOCK = 21
PUBLIC_USE_POOL = True
DEFAULT_MAC_FILTERED_BRIDGE = '%COMMON_BRIDGE%'
CUSTOM_BRIDGED_BRIDGE = '%COMMON_BRIDGE%'
DEFAULT_BRIDGE = '%COMMON_BRIDGE%'
MAX_VMS_PER_USER = 5
VMS_USER_QUOTA = {
......
[
{
"fields": {
"_cached_url": "/",
"_cached_url": "/home/",
"_content_title": "",
"_page_title": "",
"active": true,
......@@ -14,17 +14,17 @@
"meta_keywords": "",
"modification_date": "2012-11-16 14:52:19",
"navigation_extension": null,
"override_url": "/",
"override_url": "/home/",
"parent": null,
"publication_date": "2012-11-16 14:50:00",
"publication_end_date": null,
"redirect_to": "",
"rght": 2,
"site": 1,
"slug": "okeanos",
"slug": "synnefo",
"symlinked_page": null,
"template_key": "twocolwide",
"title": "Okeanos",
"title": "Synnefo",
"translation_of": null,
"tree_id": 1
},
......@@ -36,7 +36,7 @@
"ordering": 0,
"parent": 1,
"region": "main",
"text": "Welcome to Okeanos!!\r\n\r\n"
"text": "Welcome to Synnefo!!\r\n\r\n"
},
"model": "page.rawcontent",
"pk": 1
......
......@@ -3,8 +3,8 @@
"pk": 1,
"model": "sites.site",
"fields": {
"domain": "okeanos.grnet.gr",
"name": "okeanos.grnet.gr"
"domain": "%DOMAIN%",
"name": "%DOMAIN%"
}
}
]
This diff is collapsed.
This diff is collapsed.
......@@ -6,7 +6,7 @@ Fabric file for snf-deploy
"""
from __future__ import with_statement
from fabric.api import hide, env, settings, local, roles
from fabric.api import hide, env, settings, local, roles, execute
from fabric.operations import run, put, get
import fabric
import re
......@@ -853,10 +853,8 @@ def add_rapi_user():
@roles("master")
def add_nodes():
nodes = env.env.cluster_nodes.split(",")
nodes.remove(env.env.master_node)
debug(env.host, " * Adding nodes to Ganeti backend...")
for n in nodes:
for n in env.env.cluster_nodes:
add_node(n)
......
# Too many lines in module pylint: disable-msg=C0302
# Too many arguments (7/5) pylint: disable-msg=R0913
# Copyright (C) 2010, 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.
"""
Fabric file for snf-deploy
"""
from __future__ import with_statement
from fabric.api import hide, env, settings, local, roles, execute
from fabric.operations import run, put, get
import fabric
import re
import os
import shutil
import tempfile
import ast
from snfdeploy.lib import debug, Conf, Env, disable_color
from snfdeploy.utils import *
from snfdeploy import massedit
from snfdeploy.components import *
from snfdeploy import components
def setup_env(args, localenv):
"""Setup environment"""
print("Loading configuration for synnefo...")
env.env = localenv
env.target_node = args.node
env.target_component = args.component
env.target_method = args.method
env.target_role = args.role
env.dry_run = args.dry_run
env.local = args.autoconf
env.key_inject = args.key_inject
env.password = env.env.password
env.user = env.env.user
env.shell = "/bin/bash -c"
env.key_filename = args.ssh_key
env.jsonfile = "/tmp/service.json"
env.force = args.force
if args.disable_colors:
disable_color()
env.roledefs = {
"accounts": [env.env.accounts.ip],
"cyclades": [env.env.cyclades.ip],
"pithos": [env.env.pithos.ip],
"cms": [env.env.cms.ip],
"mq": [env.env.mq.ip],
"db": [env.env.db.ip],
"ns": [env.env.ns.ip],
"client": [env.env.client.ip],
"stats": [env.env.stats.ip],
"nfs": [env.env.nfs.ip],
}
env.enable_lvm = False
env.enable_drbd = False
if ast.literal_eval(env.env.create_extra_disk) and env.env.extra_disk:
env.enable_lvm = True
env.enable_drbd = True
env.roledefs.update({
"ganeti": env.env.cluster_ips,
"master": [env.env.master.ip],
})
#
#
# Those methods retrieve info from existing installation and update env
#
#
@roles("db")
def update_env_with_user_info():
user_email = env.env.user_email
result = RunComponentMethod(DB, "get_user_info_from_db")
r = re.compile(r"(\d+)[ |]*(\S+)[ |]*(\S+)[ |]*" + user_email, re.M)
match = r.search(result)
if env.dry_run:
env.user_id, env.user_auth_token, env.user_uuid = \
("dummy_uid", "dummy_user_auth_token", "dummy_user_uuid")
else:
env.user_id, env.user_auth_token, env.user_uuid = match.groups()
@roles("accounts")
def update_env_with_service_info(service="pithos"):
result = RunComponentMethod(Astakos, "get_services")
r = re.compile(r"(\d+)[ ]*%s[ ]*(\S+)" % service, re.M)
match = r.search(result)
if env.dry_run:
env.service_id, env.service_token = \
("dummy_service_id", "dummy_service_token")
else:
env.service_id, env.service_token = match.groups()
@roles("cyclades")
def update_env_with_backend_info():
cluster_name = env.env.cluster.fqdn
result = RunComponentMethod(Cyclades, "list_backends")
r = re.compile(r"(\d+)[ ]*%s.*" % cluster_name, re.M)
match = r.search(result)
if env.dry_run:
env.backend_id = "dummy_backend_id"
else:
env.backend_id, = match.groups()
#
#
# Those methods act on components after their basic setup
#
#
@roles("cyclades")
def add_ganeti_backend():
RunComponentMethod(Cyclades, "add_backend")
execute(update_env_with_backend_info)
RunComponentMethod(Cyclades, "undrain_backend")
@roles("accounts")
def add_synnefo_user():
RunComponentMethod(Astakos, "add_user")
@roles("accounts")
def activate_user():
execute(update_env_with_user_info)
RunComponentMethod(Astakos, "activate_user")
@roles("accounts")
def import_service():
f = env.jsonfile
PutToComponent(Astakos, f + ".local", f)
RunComponentMethod(Astakos, "import_service")
@roles("ns")
def update_ns_for_node(node_info):
RunComponentMethod(NS, "update_ns_for_node", node_info)
@roles("nfs")
def update_exports_for_node(node_info):
RunComponentMethod(NFS, "update_exports", node_info)
@roles("master")
def add_ganeti_node(node_info):
RunComponentMethod(Master, "add_node", node_info)