Commit 3699d1fe authored by Dimitris Aragiorgis's avatar Dimitris Aragiorgis
Browse files

deploy: Refactor components module



Up until now components were simple objects decoupled from fabric
execution. Their methods were just returning a list of bash commands
which utils module was executing in the correct context.

This patch changes this rational. Specifically:

A Component() gets initialized with an execution context that is a
configuration snapshot of the target setup, cluster and node. A
component implements the following helper methods: check, install,
prepare, configure, restart, initialize, and test. All those methods
will be executed on the target node with this order during setup.

Additionally each Component class implements admin_pre, and
admin_post methods which invoke actions on different components on
the same execution context before and after installation. For
example before a backend gets installed, its FQDN must resolve to
the master floating IP, so we have to run some actions on the ns
node and after installation we must add it to cyclades (snf-manage
backend-add on the cyclades node).

Component() inherits ComponentRunner() which practically exports the
setup() method. This will first check if the required components are
installed, will install them if not and update the status of target
node.

ComponentRunner() inherits FabricRunner() which practically wraps
basic fabric commands (put, get, run) with the correct execution
environment.

The fabfile practically gets the target nodes for each role from the
configuration and uses the roles module which knows the
role-component mapping to get the target component with the proper
execution context. Then it just invokes the setup() method.

Each component gets initialized with an execution context and uses
the config module for accessing global wide options. The context
provides node, cluster, and setup related info.

We introduce some helper decorators that wrap methods of Component
class and update its execution context (self, self.ctx):

- parse_* executes the wrapped method and parses the output.
  The desired vars (user_id, backend_id, user_uuid, etc.) are
  stored in the execution context and made available to other
  methods.

- update_admin initializes the admin roles (NS, Astakos,
  Cyclades, etc.) of the current context and make them available
  under self.NS, self.ASTAKOS, etc. These have the same execution
  context of the current components besides the target node
  which gets derived from the corresponding config.

- update_cluster_admin initializes the cluster's admin role
  and make it available under self.MASTER.

- run_cmds runs the list of returned commands in the target node

The update_ns() method of NS component gets the
self.ctx.admin_fqdn and invokes nsupdate accordingly.
Signed-off-by: default avatarDimitris Aragiorgis <dimara@grnet.gr>
parent a43cc462
......@@ -13,68 +13,193 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
import datetime
import ast
import simplejson
from snfdeploy.utils import debug
from snfdeploy.lib import Host
class SynnefoComponent(object):
REQUIRED_PACKAGES = []
def debug(self, msg, info=""):
debug(self.__class__.__name__, msg, info)
def __init__(self, node_info, env, *args, **kwargs):
""" Take a node_info and env as argument and initialize local vars """
self.node_info = node_info
self.env = env
def check(self):
""" Returns a list of bash commands that check prerequisites """
return []
def install(self):
""" Returns a list of debian packages to install """
return self.REQUIRED_PACKAGES
def prepare(self):
""" Returs a list of bash commands that prepares the component """
return []
def configure(self):
""" Must return a list of tuples (tmpl_path, replace_dict, mode) """
return []
def initialize(self):
""" Returs a list of bash commands that initialize the component """
return []
def test(self):
""" Returs a list of bash commands that test existing installation """
return []
def restart(self):
return []
#TODO: add cleanup method for each component
def clean(self):
return []
import copy
from snfdeploy import base
from snfdeploy import config
from snfdeploy import constants
from snfdeploy import context
from snfdeploy.lib import FQDN
#
# Helper decorators that wrap methods of Component
# class and update its execution context (self, self.ctx, context):
#
def parse_user_info(fn):
""" Parse the output of DB.get_user_info_from_db()
For the given user email found in config,
update user_id, user_auth_token and user_uuid attributes of context.
"""
def wrapper(*args, **kwargs):
user_email = config.user_email
result = fn(*args, **kwargs)
r = re.compile(r"(\d+)[ |]*(\S+)[ |]*(\S+)[ |]*" + user_email, re.M)
match = r.search(result)
if config.dry_run:
context.user_id, context.user_auth_token, context.user_uuid = \
("dummy_uid", "dummy_user_auth_token", "dummy_user_uuid")
elif match:
context.user_id, context.user_auth_token, context.user_uuid = \
match.groups()
else:
raise BaseException("Cannot parse info for user %s" % user_email)
return result
return wrapper
def parse_service_info(fn):
""" Parse the output of Astakos.get_services()
For the given service (found in ctx.admin_service)
updates service_id and service_token attributes of context.
"""
def wrapper(*args, **kwargs):
cl = args[0]
service = cl.ctx.admin_service
result = fn(*args, **kwargs)
r = re.compile(r"(\d+)[ ]*%s[ ]*(\S+)" % service, re.M)
match = r.search(result)
if config.dry_run:
context.service_id, context.service_token = \
("dummy_service_id", "dummy_service_token")
elif match:
context.service_id, context.service_token = \
match.groups()
else:
raise BaseException("Cannot parse info for service %s" % service)
return result
return wrapper
def parse_backend_info(fn):
""" Parse the output of Cyclades.list_backends()
For the given cluster (found in ctx.admin_cluster)
updates the backend_id attributes of context.
"""
def wrapper(*args, **kwargs):
cl = args[0]
fqdn = cl.ctx.admin_cluster.fqdn
result = fn(*args, **kwargs)
r = re.compile(r"(\d+)[ ]*%s.*" % fqdn, re.M)
match = r.search(result)
if config.dry_run:
context.backend_id = "dummy_backend_id"
elif match:
context.backend_id, = match.groups()
else:
raise BaseException("Cannot parse info for backend %s" % fqdn)
return result
return wrapper
def update_admin(fn):
""" Initializes the admin roles for each component
Initialize the admin roles (NS, Astakos, Cyclades, etc.) and make them
available under self.NS, self.ASTAKOS, etc. These have the same execution
context of the current components besides the target node which gets
derived from the corresponding config.
"""
def wrapper(*args, **kwargs):
"""If used as decorator of a class method first argument is self."""
cl = args[0]
ctx = copy.deepcopy(cl.ctx)
ctx.admin_service = cl.service
ctx.admin_cluster = cl.cluster
ctx.admin_node = cl.node
ctx.admin_fqdn = cl.fqdn
cl.NS = NS(node=ctx.ns.node, ctx=ctx)
cl.NFS = NFS(node=ctx.nfs.node, ctx=ctx)
cl.DB = DB(node=ctx.db.node, ctx=ctx)
cl.ASTAKOS = Astakos(node=ctx.astakos.node, ctx=ctx)
cl.CYCLADES = Cyclades(node=ctx.cyclades.node, ctx=ctx)
return fn(*args, **kwargs)
return wrapper
def update_cluster_admin(fn):
""" Initializes the cluster admin roles for each component
Finds the master role for the corresponding cluster
"""
def wrapper(*args, **kwargs):
"""If used as decorator of a class method first argument is self."""
cl = args[0]
ctx = copy.deepcopy(cl.ctx)
ctx.admin_cluster = cl.cluster
cl.MASTER = Master(node=ctx.master.node, ctx=ctx)
return fn(*args, **kwargs)
return wrapper
def export_and_import_service(fn):
""" Export and import synnefo service
Used in Astakos, Pithos, and Cyclades service admin_post method
"""
def wrapper(*args, **kwargs):
cl = args[0]
f = config.jsonfile
cl.export_service()
cl.get(f, f + ".local")
cl.ASTAKOS.put(f + ".local", f)
cl.ASTAKOS.import_service()
return fn(*args, **kwargs)
return wrapper
# ########################## Components ############################
# A Component() gets initialized with an execution context that is a
# configuration snapshot of the target setup, cluster and node. A
# component implements the following helper methods: check, install,
# prepare, configure, restart, initialize, and test. All those methods
# will be executed on the target node with this order during setup.
#
# Additionally each Component class implements admin_pre, and
# admin_post methods which invoke actions on different components on
# the same execution context before and after installation. For
# example before a backend gets installed, its FQDN must resolve to
# the master floating IP, so we have to run some actions on the ns
# node and after installation we must add it to cyclades (snf-manage
# backend-add on the cyclades node).
#
# Component() inherits ComponentRunner() which practically exports the
# setup() method. This will first check if the required components are
# installed, will install them if not and update the status of target
# node.
#
# ComponentRunner() inherits FabricRunner() which practically wraps
# basic fabric commands (put, get, run) with the correct execution
# environment.
#
# Each component gets initialized with an execution context and uses
# the config module for accessing global wide options. The context
# provides node, cluster, and setup related info.
class HW(SynnefoComponent):
class HW(base.Component):
@base.run_cmds
def test(self):
return [
"ping -c 1 %s" % self.node_info.ip,
"ping -c 1 %s" % self.node.ip,
"ping -c 1 www.google.com",
"apt-get update",
]
class SSH(SynnefoComponent):
class SSH(base.Component):
@base.run_cmds
def prepare(self):
return [
"mkdir -p /root/.ssh",
......@@ -82,78 +207,119 @@ class SSH(SynnefoComponent):
"echo StrictHostKeyChecking no >> /etc/ssh/ssh_config",
]
def configure(self):
def _configure(self):
files = [
"authorized_keys", "id_dsa", "id_dsa.pub", "id_rsa", "id_rsa.pub"
]
ssh = [("/root/.ssh/%s" % f, {}, {"mode": 0600}) for f in files]
return ssh
@base.run_cmds
def initialize(self):
f = "/root/.ssh/authorized_keys"
return [
"test -e {0}.bak && cat {0}.bak >> {0} || true".format(f)
]
@base.run_cmds
def test(self):
return ["ssh %s date" % self.node_info.ip]
return ["ssh %s date" % self.node.ip]
class DNS(base.Component):
@update_admin
def admin_pre(self):
self.NS.update_ns()
class DNS(SynnefoComponent):
@base.run_cmds
def prepare(self):
return [
"chattr -i /etc/resolv.conf",
"sed -i 's/^127.*$/127.0.0.1 localhost/g' /etc/hosts",
"echo %s > /etc/hostname" % self.node_info.hostname,
"hostname %s" % self.node_info.hostname
"echo %s > /etc/hostname" % self.node.hostname,
"hostname %s" % self.node.hostname
]
def configure(self):
def _configure(self):
r1 = {
"date": str(datetime.datetime.today()),
"domain": self.env.env.domain,
"ns_node_ip": self.env.env.ns.ip,
"domain": self.node.domain,
"ns_node_ip": self.ctx.ns.ip,
}
resolv = [
("/etc/resolv.conf", r1, {})
]
return resolv
@base.run_cmds
def initialize(self):
return ["chattr +i /etc/resolv.conf"]
class DDNS(SynnefoComponent):
class DDNS(base.Component):
REQUIRED_PACKAGES = [
"dnsutils",
]
@base.run_cmds
def prepare(self):
return [
"mkdir -p /root/ddns/"
]
def configure(self):
def _configure(self):
return [
("/root/ddns/" + k, {}, {}) for k in self.env.env.ddns_keys
("/root/ddns/" + k, {}, {}) for k in config.ddns_keys
]
class NS(SynnefoComponent):
class NS(base.Component):
REQUIRED_PACKAGES = [
"bind9",
]
def nsupdate(self, cmd):
alias = constants.NS
def required_components(self):
return [HW, SSH, DDNS]
def _nsupdate(self, cmd):
ret = """
nsupdate -k {0} > /dev/null <<EOF || true
server {1}
{2}
send
EOF
""".format(self.env.env.ddns_private_key, self.node_info.ip, cmd)
""".format(config.ddns_private_key, self.ctx.ns.ip, cmd)
return ret
@base.run_cmds
def update_ns(self, info=None):
if not info:
info = self.ctx.admin_fqdn
return [
self._nsupdate("update add %s" % info.arecord),
self._nsupdate("update add %s" % info.ptrrecord),
self._nsupdate("update add %s" % info.cnamerecord),
]
def add_qa_instances(self):
instances = [
("xen-test-inst1", "1.2.3.4"),
("xen-test-inst2", "1.2.3.5"),
("xen-test-inst3", "1.2.3.6"),
("xen-test-inst4", "1.2.3.7"),
]
for name, ip in instances:
info = {
"name": name,
"ip": ip,
"domain": self.node.domain
}
node_info = FQDN(**info)
self.update_ns(node_info)
@base.run_cmds
def prepare(self):
return [
"mkdir -p /etc/bind/zones",
......@@ -162,9 +328,9 @@ EOF
"chmod g+w /etc/bind/rev",
]
def configure(self):
d = self.env.env.domain
ip = self.node_info.ip
def _configure(self):
d = self.node.domain
ip = self.node.ip
return [
("/etc/bind/named.conf.local", {"domain": d}, {}),
("/etc/bind/zones/example.com",
......@@ -176,62 +342,20 @@ EOF
("/etc/bind/rev/synnefo.in-addr.arpa.zone", {"domain": d}, {}),
("/etc/bind/rev/synnefo.ip6.arpa.zone", {"domain": d}, {}),
("/etc/bind/named.conf.options",
{"node_ips": ";".join(self.env.env.ips)}, {}),
{"node_ips": ";".join(config.all_ips)}, {}),
("/root/ddns/ddns.key", {}, {"remote": "/etc/bind/ddns.key"}),
]
def update_cnamerecord(self, node_info):
return self.nsupdate("update add %s" % node_info.cnamerecord)
def update_arecord(self, node_info):
return self.nsupdate("update add %s" % node_info.arecord)
def update_ptrrecord(self, node_info):
return self.nsupdate("update add %s" % node_info.ptrrecord)
def update_ns_for_node(self, node_info):
return [
self.update_arecord(node_info),
self.update_cnamerecord(node_info),
self.update_ptrrecord(node_info)
]
def add_qa_instances(self):
instances = [
("xen-test-inst1", "1.2.3.4"),
("xen-test-inst2", "1.2.3.5"),
("xen-test-inst3", "1.2.3.6"),
("xen-test-inst4", "1.2.3.7"),
]
for name, ip in instances:
n = Host(name, ip, None, "vm." + self.env.env.domain, None, None)
self.update_ns_for_node(n)
def initialize(self):
a = [self.update_arecord(n)
for n in self.env.env.nodes_info.values()]
ptr = [self.update_ptrrecord(n)
for n in self.env.env.nodes_info.values()]
cnames = [self.update_cnamerecord(n)
for n in self.env.env.roles_info.values()]
return a + ptr + cnames
@base.run_cmds
def restart(self):
return ["/etc/init.d/bind9 restart"]
def test(self):
n = ["host %s localhost" % i.fqdn
for i in self.env.env.nodes_info.values()]
a = ["host %s localhost" % i.fqdn
for i in self.env.env.roles_info.values()]
return n + a
class APT(SynnefoComponent):
class APT(base.Component):
""" Setup apt repos and check fqdns """
REQUIRED_PACKAGES = ["curl"]
@base.run_cmds
def prepare(self):
return [
"echo 'APT::Install-Suggests \"false\";' >> /etc/apt/apt.conf",
......@@ -239,26 +363,38 @@ class APT(SynnefoComponent):
apt-key add -",
]
def configure(self):
def _configure(self):
return [
("/etc/apt/sources.list.d/synnefo.wheezy.list", {}, {})
]
@base.run_cmds
def initialize(self):
return [
"apt-get update",
]
class MQ(SynnefoComponent):
class MQ(base.Component):
REQUIRED_PACKAGES = ["rabbitmq-server"]
alias = constants.MQ
def required_components(self):
return [HW, SSH, DNS, APT]
@update_admin
def admin_pre(self):
self.NS.update_ns()
@base.run_cmds
def check(self):
return ["ping -c 1 mq.%s" % self.env.env.domain]
return ["ping -c 1 %s" % self.node.fqdn]
@base.run_cmds
def initialize(self):
u = self.env.env.synnefo_user
p = self.env.env.synnefo_rabbitmq_passwd
u = config.synnefo_user
p = config.synnefo_rabbitmq_passwd
return [
"rabbitmqctl add_user %s %s" % (u, p),
"rabbitmqctl set_permissions %s \".*\" \".*\" \".*\"" % u,
......@@ -267,12 +403,24 @@ class MQ(SynnefoComponent):
]
class DB(SynnefoComponent):
class DB(base.Component):
REQUIRED_PACKAGES = ["postgresql"]
alias = constants.DB
def required_components(self):
return [HW, SSH, DNS, APT]
@update_admin
def admin_pre(self):
self.NS.update_ns()
@base.run_cmds
def check(self):
return ["ping -c 1 db.%s" % self.env.env.domain]
return ["ping -c 1 %s" % self.node.fqdn]
@parse_user_info
@base.run_cmds
def get_user_info_from_db(self):
cmd = """
cat > /tmp/psqlcmd <<EOF
......@@ -281,44 +429,52 @@ where auth_user.id = im_astakosuser.user_ptr_id and auth_user.email = '{0}';
EOF
su - postgres -c "psql -w -d snf_apps -f /tmp/psqlcmd"
""".format(self.env.env.user_email)
""".format(config.user_email)
return [cmd]
def allow_access_in_db(self, node_info, user="all", method="md5"):
@base.run_cmds
def allow_db_access(self):
user = "all"
method = "md5"
ip = self.ctx.admin_node.ip
f = "/etc/postgresql/*/main/pg_hba.conf"
cmd1 = "echo host all %s %s/32 %s >> %s" % \
(user, node_info.ip, method, f)
(user, ip, method, f)
cmd2 = "sed -i 's/\(host.*127.0.0.1.*\)md5/\\1trust/' %s" % f
return [cmd1, cmd2] + self.restart()
return [cmd1, cmd2]
def configure(self):
u = self.env.env.synnefo_user
p = self.env.env.synnefo_db_passwd
def _configure(self):
u = config.synnefo_user
p = config.synnefo_db_passwd
replace = {"synnefo_user": u, "synnefo_db_passwd": p}
return [
("/tmp/db-init.psql", replace, {}),
]
@base.check_if_testing
def make_db_fast(self):
f = "/etc/postgresql/*/main/postgresql.conf"
opts = "fsync=off\nsynchronous_commit=off\nfull_page_writes=off\n"
return ["""echo -e "%s" >> %s""" % (opts, f)]
@base.run_cmds
def prepare(self):
f = "/etc/postgresql/*/main/postgresql.conf"
return [
"""echo "listen_addresses = '*'" >> %s""" % f,
]
ret = ["""echo "listen_addresses = '*'" >> %s""" % f]
return ret + self.make_db_fast()
@base.run_cmds
def initialize(self):
script = "/tmp/db-init.psql"
cmd = "su - postgres -c \"psql -w -f %s\" " % script
return [cmd]
@base.run_cmds
def restart(self):
return ["/etc/init.d/postgresql restart"]
@base.run_cmds
def destroy_db(self):
return [
"""su - postgres -c ' psql -w -c "drop database snf_apps" '""",
......@@ -326,8 +482,25 @@ su - postgres -c "psql -w -d snf_apps -f /tmp/psqlcmd"
]
class Ganeti(SynnefoComponent):
class VMC(base.Component):
def extra_components(self):
if self.cluster.synnefo:
return [Image, GTools, GanetiCollectd]
else:
return []
def required_components(self):
return [
HW, SSH, DNS, DDNS, APT, Mount, Ganeti, Network,
] + self.extra_components()
@update_cluster_admin
def admin_post(self):
self.MASTER.add_node(self.node)
class Ganeti(