Statistics
| Branch: | Tag: | Revision:

root / snf-deploy / snfdeploy / __init__.py @ e23023e8

History | View | Annotate | Download (18.9 kB)

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

    
34
import time
35
import os
36
import argparse
37
import sys
38
import re
39
import random
40
import ast
41
import glob
42
from snfdeploy.lib import check_pidfile, create_dir, get_default_route, \
43
    random_mac, Conf, Env, Status
44
# from snfdeploy import fabfile
45
from snfdeploy import fabfile2 as fabfile
46
from fabric.api import hide, settings, execute, show
47

    
48

    
49
def print_available_actions(command):
50

    
51
    if command == "keygen":
52
        print """
53
Usage: snf-deploy keygen [--force]
54

55
  Generate new ssh keys (both rsa and dsa keypairs)
56

57
  """
58

    
59
    if command == "vcluster":
60
        print """
61
Usage: snf-deploy vcluster
62

63
  Run the following actions concerning the local virtual cluster:
64

65
    - Download base image and create additional disk \
66
(if --create-extra-disk is passed)
67
    - Does all the network related actions (bridge, iptables, NAT)
68
    - Launches dnsmasq for dhcp server on bridge
69
    - Creates the virtual cluster (with kvm)
70

71
  """
72

    
73
    if command == "backend":
74
        print """
75
Usage: snf-deploy backend
76

77
  Run the following actions concerning a ganeti backend:
78

79
    - Create and add a backend to cyclades
80

81
  """
82

    
83
    if command == "run":
84
        print """
85
Usage: snf-deploy run <action> [<action>...]
86

87
  Run any of the following fabric commands:
88

89
    Role setup:
90

91
      setup_ns_role
92
      setup_nfs_role
93
      setup_db_role
94
      setup_mq_role
95
      setup_astakos_role
96
      setup_pithos_role
97
      setup_cyclades_role
98
      setup_cms_role
99
      setup_ganeti_role
100
      setup_master_role
101
      setup_stats_role
102
      setup_client_role
103

104
    Helper commands:
105

106
      update_env_with_user_info
107
      update_env_with_service_info
108
      update_env_with_backend_info
109

110
    Admin commands:
111

112
      update_ns_for_node
113
      update_exports_for_node
114
      allow_db_access
115
      add_ganeti_backend
116
      add_synnefo_user
117
      activate_user
118
      set_default_quota
119
      add_public_networks
120
      add_image
121

122

123
    Custom command:
124

125
      setup --node NODE [--role ROLE | --method METHOD --component COMPONENT]
126

127
  """
128

    
129
    sys.exit(1)
130

    
131

    
132
def create_dnsmasq_files(args, env):
133

    
134
    print("Customize dnsmasq..")
135
    out = env.dns
136

    
137
    hostsfile = open(out + "/dhcp-hostsfile", "w")
138
    optsfile = open(out + "/dhcp-optsfile", "w")
139
    conffile = open(out + "/conf-file", "w")
140

    
141
    for node, info in env.nodes_info.iteritems():
142
        # serve ip and hostname to nodes
143
        hostsfile.write("%s,%s,%s,2m\n" % (info.mac, info.ip, info.hostname))
144

    
145
    hostsfile.write("52:54:56:*:*:*,ignore\n")
146

    
147
    # Netmask
148
    optsfile.write("1,%s\n" % env.net.netmask)
149
    # Gateway
150
    optsfile.write("3,%s\n" % env.gateway)
151
    # Namesevers
152
    optsfile.write("6,%s\n" % "8.8.8.8")
153

    
154
    dnsconf = """
155
user=dnsmasq
156
bogus-priv
157
no-poll
158
no-negcache
159
leasefile-ro
160
bind-interfaces
161
except-interface=lo
162
dhcp-fqdn
163
no-resolv
164
# disable DNS
165
port=0
166
""".format(env.ns.ip)
167

    
168
    dnsconf += """
169
# serve domain and search domain for resolv.conf
170
domain={5}
171
interface={0}
172
dhcp-hostsfile={1}
173
dhcp-optsfile={2}
174
dhcp-range={0},{4},static,2m
175
""".format(env.bridge, hostsfile.name, optsfile.name,
176
           env.domain, env.net.network, env.domain)
177

    
178
    conffile.write(dnsconf)
179

    
180
    hostsfile.close()
181
    optsfile.close()
182
    conffile.close()
183

    
184

    
185
def cleanup(args, env):
186
    print("Cleaning up bridge, NAT, resolv.conf...")
187

    
188
    for f in os.listdir(env.run):
189
        if re.search(".pid$", f):
190
            check_pidfile(os.path.join(env.run, f))
191

    
192
    create_dir(env.run, True)
193
    # create_dir(env.cmd, True)
194
    cmd = """
195
    iptables -t nat -D POSTROUTING -s {0} -o {1} -j MASQUERADE
196
    echo 0 > /proc/sys/net/ipv4/ip_forward
197
    iptables -D INPUT -i {2} -j ACCEPT
198
    iptables -D FORWARD -i {2} -j ACCEPT
199
    iptables -D OUTPUT -o {2} -j ACCEPT
200
    """.format(env.subnet, get_default_route()[1], env.bridge)
201
    os.system(cmd)
202

    
203
    cmd = """
204
    ip link show {0} && ip addr del {1}/{2} dev {0}
205
    sleep 1
206
    ip link set {0} down
207
    sleep 1
208
    brctl delbr {0}
209
    """.format(env.bridge, env.gateway, env.net.prefixlen)
210
    os.system(cmd)
211

    
212

    
213
def network(args, env):
214
    print("Create bridge..Add gateway IP..Activate NAT.."
215
          "Append NS options to resolv.conf")
216

    
217
    cmd = """
218
    ! ip link show {0} && brctl addbr {0} && ip link set {0} up
219
    sleep 1
220
    ip link set promisc on dev {0}
221
    ip addr add {1}/{2} dev {0}
222
    """.format(env.bridge, env.gateway, env.net.prefixlen)
223
    os.system(cmd)
224

    
225
    cmd = """
226
    iptables -t nat -A POSTROUTING -s {0} -o {1} -j MASQUERADE
227
    echo 1 > /proc/sys/net/ipv4/ip_forward
228
    iptables -I INPUT 1 -i {2} -j ACCEPT
229
    iptables -I FORWARD 1 -i {2} -j ACCEPT
230
    iptables -I OUTPUT 1 -o {2} -j ACCEPT
231
    """.format(env.subnet, get_default_route()[1], env.bridge)
232
    os.system(cmd)
233

    
234

    
235
def image(args, env):
236
    #FIXME: Create a clean wheezy image and use it for vcluster
237
    if env.os == "ubuntu":
238
        url = env.ubuntu_image_url
239
    else:
240
        url = env.squeeze_image_url
241

    
242
    disk0 = "{0}/{1}.disk0".format(env.images, env.os)
243
    disk1 = "{0}/{1}.disk1".format(env.images, env.os)
244

    
245
    if url and not os.path.exists(disk0):
246
        cmd = "wget {0} -O {1}".format(url, disk0)
247
        os.system(cmd)
248

    
249
    if ast.literal_eval(env.create_extra_disk) and not os.path.exists(disk1):
250
        if env.lvg:
251
            cmd = "lvcreate -L30G -n{0}.disk1 {1}".format(env.os, env.lvg)
252
            os.system(cmd)
253
            cmd = "ln -s /dev/{0}/{1}.disk1 {2}".format(env.lvg, env.os, disk1)
254
            os.system(cmd)
255
        else:
256
            cmd = "dd if=/dev/zero of={0} bs=10M count=3000".format(disk1)
257
            os.system(cmd)
258

    
259

    
260
def fabcommand(args, env, actions, nodes=[]):
261
    levels = ["status", "aborts", "warnings", "running",
262
              "stdout", "stderr", "user", "debug"]
263

    
264
    level_aliases = {
265
        "output": ["stdout", "stderr"],
266
        "everything": ["warnings", "running", "user", "output"]
267
    }
268

    
269
    lhide = level_aliases["everything"]
270
    lshow = []
271

    
272
    if args.verbose == 1:
273
        lshow = levels[:3]
274
        lhide = levels[3:]
275
    elif args.verbose == 2:
276
        lshow = levels[:4]
277
        lhide = levels[4:]
278
    elif args.verbose >= 3 or args.debug:
279
        lshow = levels
280
        lhide = []
281

    
282
#   fabcmd += " --fabfile {4}/fabfile.py \
283
# setup_env:confdir={0},packages={1},templates={2},cluster_name={3},\
284
# autoconf={5},disable_colors={6},key_inject={7} \
285
# ".format(args.confdir, env.packages, env.templates, args.cluster_name,
286
#          env.lib, args.autoconf, args.disable_colors, args.key_inject)
287

    
288
    if nodes:
289
        ips = [env.nodes_info[n].ip for n in nodes]
290

    
291
    fabfile.setup_env(args, env)
292
    with settings(hide(*lhide), show(*lshow)):
293
        print " ".join(actions)
294
        for a in actions:
295
            fn = getattr(fabfile, a)
296
            if nodes:
297
                execute(fn, hosts=ips)
298
            else:
299
                execute(fn)
300

    
301

    
302
def cluster(args, env):
303
    for hostname, mac in env.node2mac.iteritems():
304
        launch_vm(args, env, hostname, mac)
305

    
306
    time.sleep(30)
307
    os.system("reset")
308

    
309

    
310
def launch_vm(args, env, hostname, mac):
311
    check_pidfile("%s/%s.pid" % (env.run, hostname))
312

    
313
    print("Launching cluster node {0}..".format(hostname))
314
    os.environ["BRIDGE"] = env.bridge
315
    if args.vnc:
316
        graphics = "-vnc :{0}".format(random.randint(1, 1000))
317
    else:
318
        graphics = "-nographic"
319

    
320
    disks = """ \
321
-drive file={0}/{1}.disk0,format=raw,if=none,id=drive0,snapshot=on \
322
-device virtio-blk-pci,drive=drive0,id=virtio-blk-pci.0 \
323
""".format(env.images, env.os)
324

    
325
    if ast.literal_eval(env.create_extra_disk):
326
        disks += """ \
327
-drive file={0}/{1}.disk1,format=raw,if=none,id=drive1,snapshot=on \
328
-device virtio-blk-pci,drive=drive1,id=virtio-blk-pci.1 \
329
""".format(env.images, env.os)
330

    
331
    ifup = env.lib + "/ifup"
332
    nics = """ \
333
-netdev tap,id=netdev0,script={0},downscript=no \
334
-device virtio-net-pci,mac={1},netdev=netdev0,id=virtio-net-pci.0 \
335
-netdev tap,id=netdev1,script={0},downscript=no \
336
-device virtio-net-pci,mac={2},netdev=netdev1,id=virtio-net-pci.1 \
337
-netdev tap,id=netdev2,script={0},downscript=no \
338
-device virtio-net-pci,mac={3},netdev=netdev2,id=virtio-net-pci.2 \
339
""".format(ifup, mac, random_mac(), random_mac())
340

    
341
    cmd = """
342
/usr/bin/kvm -name {0} -pidfile {1}/{0}.pid -balloon virtio -daemonize \
343
-monitor unix:{1}/{0}.monitor,server,nowait -usbdevice tablet -boot c \
344
{2} \
345
{3} \
346
-m {4} -smp {5} {6} \
347
""".format(hostname, env.run, disks, nics, args.mem, args.smp, graphics)
348
    print cmd
349
    os.system(cmd)
350

    
351

    
352
def dnsmasq(args, env):
353
    check_pidfile(env.run + "/dnsmasq.pid")
354
    cmd = "dnsmasq --pid-file={0}/dnsmasq.pid --conf-file={1}/conf-file"\
355
        .format(env.run, env.dns)
356
    os.system(cmd)
357

    
358

    
359
def get_packages(args, env):
360
    if env.package_url:
361
        os.system("rm {0}/*.deb".format(env.packages))
362
        os.system("wget -r --level=1 -nH --no-parent --cut-dirs=4 {0} -P {1}"
363
                  .format(env.package_url, env.packages))
364

    
365

    
366
def parse_options():
367
    parser = argparse.ArgumentParser()
368

    
369
    # Directories to load/store config
370
    parser.add_argument("-c", dest="confdir",
371
                        default="/etc/snf-deploy",
372
                        help="Directory to find default configuration")
373
    parser.add_argument("--dry-run", dest="dry_run",
374
                        default=False, action="store_true",
375
                        help="Do not execute or write anything.")
376
    parser.add_argument("-v", dest="verbose",
377
                        default=0, action="count",
378
                        help="Increase verbosity.")
379
    parser.add_argument("-d", dest="debug",
380
                        default=False, action="store_true",
381
                        help="Debug mode")
382
    parser.add_argument("--autoconf", dest="autoconf",
383
                        default=False, action="store_true",
384
                        help="In case of all in one auto conf setup")
385

    
386
    # virtual cluster related options
387
    parser.add_argument("--mem", dest="mem",
388
                        default=2024,
389
                        help="Memory for every virtual node")
390
    parser.add_argument("--smp", dest="smp",
391
                        default=1,
392
                        help="Virtual CPUs for every virtual node")
393
    parser.add_argument("--vnc", dest="vnc",
394
                        default=False, action="store_true",
395
                        help="Whether virtual nodes will have a vnc "
396
                             "console or not")
397
    parser.add_argument("--force", dest="force",
398
                        default=False, action="store_true",
399
                        help="Force things (creation of key pairs"
400
                             " do not abort execution if something fails")
401

    
402
    parser.add_argument("-i", "--ssh-key", dest="ssh_key",
403
                        default=None,
404
                        help="Path of an existing ssh key to use")
405

    
406
    parser.add_argument("--no-key-inject", dest="key_inject",
407
                        default=True, action="store_false",
408
                        help="Whether to inject ssh key pairs to hosts")
409

    
410
    # backend related options
411
    parser.add_argument("--cluster-name", dest="cluster_name",
412
                        default="ganeti1",
413
                        help="The cluster name in ganeti.conf")
414

    
415
    # backend related options
416
    parser.add_argument("--cluster-node", dest="cluster_node",
417
                        default=None,
418
                        help="The node to add to the existing cluster")
419

    
420
    # options related to custom setup
421
    parser.add_argument("--component", dest="component",
422
                        default=None,
423
                        help="The component class")
424

    
425
    parser.add_argument("--method", dest="method",
426
                        default=None,
427
                        help="The component method")
428

    
429
    parser.add_argument("--role", dest="role",
430
                        default=None,
431
                        help="The target node's role")
432

    
433
    parser.add_argument("--node", dest="node",
434
                        default="node1",
435
                        help="The target node")
436

    
437
    # available commands
438
    parser.add_argument("command", type=str,
439
                        choices=["packages", "vcluster", "cleanup",
440
                                 "run", "test", "all", "keygen"],
441
                        help="Run on of the supported deployment commands")
442

    
443
    # available actions for the run command
444
    parser.add_argument("actions", type=str, nargs="*",
445
                        help="Run one or more of the supported subcommands")
446

    
447
    # disable colors in terminal
448
    parser.add_argument("--disable-colors", dest="disable_colors",
449
                        default=False, action="store_true",
450
                        help="Disable colors in terminal")
451

    
452
    return parser.parse_args()
453

    
454

    
455
def get_actions(*args):
456
    actions = {
457
        "backend": [
458
            "setup_master_role",
459
            "setup_ganeti_role",
460
            "add_ganeti_backend",
461
        ],
462
        "ganeti": [
463
            "setup_ns_role",
464
            "setup_nfs_role",
465
            "setup_master_role",
466
            "setup_ganeti_role",
467
        ],
468
        "all": [
469
            "setup_ns_role",
470
            "setup_nfs_role",
471
            "setup_db_role",
472
            "setup_mq_role",
473
            "setup_astakos_role",
474
            "setup_pithos_role",
475
            "setup_cyclades_role",
476
            "setup_cms_role",
477
            "setup_master_role",
478
            "setup_ganeti_role",
479
            "setup_stats_role",
480
            "set_default_quota",
481
            "add_ganeti_backend",
482
            "add_public_networks",
483
            "add_synnefo_user",
484
            "activate_user",
485
            "setup_client_role",
486
            "add_image",
487
        ],
488

    
489
    }
490

    
491
    ret = []
492
    for x in args:
493
        ret += actions[x]
494

    
495
    return ret
496

    
497

    
498
def must_create_keys(env):
499
    """Check if we ssh keys already exist
500

501
    """
502
    d = os.path.join(env.templates, "root/.ssh")
503
    auth_keys_exists = os.path.exists(os.path.join(d, "authorized_keys"))
504
    dsa_exists = os.path.exists(os.path.join(d, "id_dsa"))
505
    dsa_pub_exists = os.path.exists(os.path.join(d, "id_dsa.pub"))
506
    rsa_exists = os.path.exists(os.path.join(d, "id_rsa"))
507
    rsa_pub_exists = os.path.exists(os.path.join(d, "id_rsa.pub"))
508
    # If any of the above doesn't exist return True
509
    return not (dsa_exists and dsa_pub_exists
510
                and rsa_exists and rsa_pub_exists
511
                and auth_keys_exists)
512

    
513

    
514
def do_create_keys(args, env):
515
    d = os.path.join(env.templates, "root/.ssh")
516
    a = os.path.join(d, "authorized_keys")
517
    # Delete old keys
518
    for filename in os.listdir(d):
519
        os.remove(os.path.join(d, filename))
520
    # Generate new keys
521
    for t in ("dsa", "rsa"):
522
        f = os.path.join(d, "id_" + t)
523
        cmd = 'ssh-keygen -q -t {0} -f {1} -N ""'.format(t, f)
524
        os.system(cmd)
525
        cmd = 'cat {0}.pub >> {1}'.format(f, a)
526
        os.system(cmd)
527

    
528

    
529
def must_create_ddns_keys(env):
530
    d = os.path.join(env.templates, "root/ddns")
531
    key_exists = glob.glob(os.path.join(d, "Kddns*key"))
532
    private_exists = glob.glob(os.path.join(d, "Kddns*private"))
533
    bind_key_exists = os.path.exists(os.path.join(d, "ddns.key"))
534
    return not (key_exists and private_exists and bind_key_exists)
535

    
536

    
537
def find_ddns_key_files(env):
538
    d = os.path.join(env.templates, "root/ddns")
539
    keys = glob.glob(os.path.join(d, "Kddns*"))
540
    # Here we must have a key!
541
    return map(os.path.basename, keys)
542

    
543

    
544
def do_create_ddns_keys(args, env):
545
    d = os.path.join(env.templates, "root/ddns")
546
    if not os.path.exists(d):
547
        os.mkdir(d)
548
    for filename in os.listdir(d):
549
        os.remove(os.path.join(d, filename))
550
    cmd = """
551
dnssec-keygen -a HMAC-MD5 -b 128 -K {0} -r /dev/urandom -n USER DDNS_UPDATE
552
key=$(cat {0}/Kddns_update*.key | awk '{{ print $7 }}')
553
cat > {0}/ddns.key <<EOF
554
key DDNS_UPDATE {{
555
        algorithm HMAC-MD5.SIG-ALG.REG.INT;
556
        secret "$key";
557
}};
558
EOF
559
""".format(d)
560
    os.system(cmd)
561

    
562

    
563
def main():
564
    args = parse_options()
565

    
566
    conf = Conf(args)
567
    env = Env(conf)
568
    env.status = Status(args)
569

    
570
    create_dir(env.run, False)
571
    create_dir(env.dns, False)
572

    
573
    # Check if there are keys to use
574
    if args.command == "keygen":
575
        if not args.force:
576
            if not must_create_keys(env) or not must_create_ddns_keys(env):
577
                print "Keys already exist.."
578
                print "To override existing ones use --force."
579
                return 1
580
        do_create_keys(args, env)
581
        do_create_ddns_keys(args, env)
582
        return 0
583
    else:
584
        if ((args.key_inject and not args.ssh_key and
585
             must_create_keys(env)) or must_create_ddns_keys(env)):
586
            print "No ssh/ddns keys to use. Run `snf-deploy keygen' first."
587
            return 1
588
        env.ddns_keys = find_ddns_key_files(env)
589
        env.ddns_private_key = "/root/ddns/" + env.ddns_keys[0]
590

    
591
    if args.command == "test":
592
        conf.print_config()
593

    
594
    if args.command == "cleanup":
595
        cleanup(args, env)
596

    
597
    if args.command == "packages":
598
        create_dir(env.packages, True)
599
        get_packages(args, env)
600

    
601
    if args.command == "vcluster":
602
        image(args, env)
603
        network(args, env)
604
        create_dnsmasq_files(args, env)
605
        dnsmasq(args, env)
606
        cluster(args, env)
607

    
608
    if args.command == "backend":
609
        actions = get_actions("backend")
610
        fabcommand(args, env, actions)
611

    
612
    if args.command == "ganeti":
613
        actions = get_actions("ganeti")
614
        fabcommand(args, env, actions)
615

    
616
    if args.command == "all":
617
        actions = get_actions("all")
618
        fabcommand(args, env, actions)
619

    
620
    if args.command == "run":
621
        if not args.actions:
622
            print_available_actions(args.command)
623
        else:
624
            fabcommand(args, env, args.actions)
625

    
626
    return 0
627

    
628
if __name__ == "__main__":
629
    sys.exit(main())