ansiblecube.py 6.57 KB
Newer Older
1
2
3
4
# -*- coding: utf-8 -*-
# vim: ai ts=4 sts=4 et sw=4 nu

import os
thiolliere's avatar
thiolliere committed
5
import json
6
7
8
import tempfile
import posixpath

9
10
import yaml

11
12
from data import mirror
from backend.content import get_content
13
from backend.catalog import CATALOGS, get_catalogs
14
15

ansiblecube_path = "/var/lib/ansible/local"
16
17
18
19
20
# update CATALOGS to include our in-qemu local URL
for catalog in CATALOGS:
    catalog.update(
        {"local_url": "file:///home/pi/catalog_{}.yml".format(catalog["name"])}
    )
21
22
23


def run(machine, tags, extra_vars={}, secret_keys=[]):
24
    """ run ansiblecube in given machine with specified tags and extra-vars """
25
26
27

    # predefined defaults we want to superseed whichever in ansiblecube
    ansible_vars = {
28
29
30
        "mirror": mirror,
        "catalogs": CATALOGS,
        "kernel_version": get_content("raspbian_image").get("kernel_version"),
31
32
33
34
35
    }
    ansible_vars.update(extra_vars)

    # save extra_vars to a file on guest
    extra_vars_path = posixpath.join(ansiblecube_path, "extra_vars.json")
36
    with tempfile.NamedTemporaryFile("w", delete=False) as fp:
37
38
39
40
41
42
        json.dump(ansible_vars, fp, indent=4)
        fp.close()
        machine.put_file(fp.name, extra_vars_path)
        os.unlink(fp.name)

    # prepare ansible command
43
44
45
46
47
48
49
    ansible_cmd = [
        "/usr/local/bin/ansible-playbook",
        "--inventory hosts",
        "--tags {}".format(",".join(tags)),
        '--extra-vars="@{}"'.format(extra_vars_path),
        "main.yml",
    ]
50
51
52
53

    # display sent configuration to logger
    machine._logger.std("ansiblecube extra_vars")
    machine._logger.raw_std(
54
55
56
57
58
        json.dumps(
            {k: "****" if k in secret_keys else v for k, v in ansible_vars.items()},
            indent=4,
        )
    )
59
60

    # review the list of tasks so the logger can  use it to track progression
61
62
63
64
65
66
67
    tasks_cmd = ansible_cmd[0:1] + ["--list-tasks"] + ansible_cmd[1:]
    machine.exec_cmd(
        'sh -c \'cd {path} && tasks=$({cmd} | paste -sd "^" -) '
        '&& echo "### TASKS ### $tasks"\''.format(
            path=ansiblecube_path, cmd=" ".join(tasks_cmd)
        )
    )
68
69

    # run ansible-playbook
70
71
72
73
74
75
    ansible_cmd = ansible_cmd[0:1] + ["-vvv"] + ansible_cmd[1:]  # verbose
    machine.exec_cmd(
        "sudo sh -c 'cd {path} && {cmd}'".format(
            path=ansiblecube_path, cmd=" ".join(ansible_cmd)
        )
    )
76
77
78


def run_for_image(machine, root_partition_size, disk_size):
79
80
    """ initial launch of a bare raspbian to create a base (master) image """
    tags = ["master", "rename", "setup"]
thiolliere's avatar
thiolliere committed
81
82

    machine.exec_cmd("sudo apt-get update")
83
    # install ansible dependencies (packages)
84
85
86
    machine.exec_cmd(
        "sudo apt-get install -y " "python-dev libffi-dev libssl-dev git lsb-release"
    )
87
    # install the latest pip
88
    machine.exec_cmd("wget https://bootstrap.pypa.io/get-pip.py " "-O /tmp/get-pip.py")
89
90
    machine.exec_cmd("sudo python /tmp/get-pip.py")
    # install latest ansible and important python dependencies
91
92
    machine.exec_cmd(
        "sudo sudo python -m pip install -U "
93
        "pip virtualenv jinja2 paramiko pyyaml httplib2 ansible==2.6.6"
94
    )
thiolliere's avatar
thiolliere committed
95

96
    # prepare ansible files
thiolliere's avatar
thiolliere committed
97
    machine.exec_cmd("sudo mkdir --mode 0755 -p /etc/ansible")
98
99
100
    machine.exec_cmd(
        "sudo cp {path}/hosts /etc/ansible/hosts".format(path=ansiblecube_path)
    )
101
102
103
104
    machine.exec_cmd("sudo mkdir --mode 0755 -p /etc/ansible/facts.d")

    # default configuration on the master: all contents enabled for setup tag
    extra_vars, _ = build_extra_vars(
105
106
107
108
109
        name="default",
        timezone="UTC",
        language="en",
        language_name="English",
        wifi_pwd="",
110
        edupi=True,
rgaudin's avatar
rgaudin committed
111
        edupi_resources=None,
112
113
114
        wikifundi_languages=["en", "fr"],
        aflatoun_languages=["en", "fr"],
        kalite_languages=["en", "fr", "es"],
115
116
117
        packages=[],
        admin_account=None,
        root_partition_size=root_partition_size,
118
119
        disk_size=disk_size,
    )
thiolliere's avatar
thiolliere committed
120

121
    run(machine, tags, extra_vars)
thiolliere's avatar
thiolliere committed
122

123

124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def build_extra_vars(
    name,
    timezone,
    language,
    language_name,
    wifi_pwd,
    edupi,
    edupi_resources,
    wikifundi_languages,
    aflatoun_languages,
    kalite_languages,
    packages,
    admin_account,
    root_partition_size,
    disk_size,
):
    """ extra-vars friendly format of the ansiblecube configuration """
141
142

    extra_vars = {
143
144
        "root_partition_size": root_partition_size,
        "disk_size": disk_size,
145
146
147
148
149
150
151
152
153
154
155
156
157
        "project_name": name,
        "timezone": timezone,
        "language": language,
        "language_name": language_name,
        "kalite_languages": kalite_languages,
        "wikifundi_languages": wikifundi_languages,
        "aflatoun_languages": aflatoun_languages,
        "edupi": edupi,
        "edupi_has_resources": bool(edupi_resources),
        "packages": packages,
        "custom_branding_path": "/tmp",
        "admin_account": "admin",
        "admin_password": "admin-password",
158
159
160
    }

    if wifi_pwd:
161
        extra_vars.update({"wpa_pass": wifi_pwd})
thiolliere's avatar
thiolliere committed
162

163
    if admin_account is not None:
164
165
166
167
168
169
170
        extra_vars.update(
            {
                "admin_account": admin_account["login"],
                "admin_password": admin_account["pwd"],
            }
        )
        secret_keys = ["admin_account", "admin_password"]
171
    else:
172
173
174
175
176
        secret_keys = []

    return extra_vars, secret_keys


177
178
def run_phase_one(machine, extra_vars, secret_keys, logo=None, favicon=None, css=None):
    """ run ansiblecube in machine to configure requested softwares """
179

180
    tags = ["resize", "rename", "reconfigure"]
181
182

    # copy branding files if set
183
    branding = {"favicon.png": favicon, "header-logo.png": logo, "style.css": css}
184
185
186
187
188
189

    has_custom_branding = False
    for fname, item in [(k, v) for k, v in branding.items() if v is not None]:
        has_custom_branding = True
        machine.put_file(item, "/tmp/{}".format(fname))

190
    extra_vars.update({"has_custom_branding": has_custom_branding})
191

192
193
    # save YAML catalogs into local files inside VM for use by ideascube
    for index, catalog in enumerate(CATALOGS):
194
        with tempfile.NamedTemporaryFile(suffix=".yml", delete=False) as fd:
195
196
197
198
199
200
201
202
            yaml.safe_dump(
                get_catalogs(machine._logger)[index],
                fd,
                default_flow_style=False,
                allow_unicode=True,
                encoding="utf-8",
            )
            machine.put_file(fd.name, catalog["local_url"].replace("file://", ""))
203
            os.unlink(fd.name)
204

205
206
207
208
    run(machine, tags, extra_vars, secret_keys)


def run_phase_two(machine, extra_vars, secret_keys):
209
    """ run ansiblecube to complete config now that content is in data part """
210

211
    tags = ["move-content", "seal"]
212

213
    run(machine, tags, extra_vars, secret_keys)