ansiblecube.py 6.65 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
from data import mirror
12
from version import get_version_str
13
from backend.content import get_content
14
from backend.catalog import CATALOGS, get_catalogs
15
16

ansiblecube_path = "/var/lib/ansible/local"
17
18
19
20
21
# 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"])}
    )
22
23
24


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

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

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

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

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

    # review the list of tasks so the logger can  use it to track progression
63
64
65
66
67
68
69
    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)
        )
    )
70
71

    # run ansible-playbook
72
73
74
75
76
77
    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)
        )
    )
78
79
80


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

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

98
    # prepare ansible files
thiolliere's avatar
thiolliere committed
99
    machine.exec_cmd("sudo mkdir --mode 0755 -p /etc/ansible")
100
101
102
    machine.exec_cmd(
        "sudo cp {path}/hosts /etc/ansible/hosts".format(path=ansiblecube_path)
    )
103
104
105
106
    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(
107
108
109
110
111
        name="default",
        timezone="UTC",
        language="en",
        language_name="English",
        wifi_pwd="",
112
        edupi=True,
rgaudin's avatar
rgaudin committed
113
        edupi_resources=None,
114
115
116
        wikifundi_languages=["en", "fr"],
        aflatoun_languages=["en", "fr"],
        kalite_languages=["en", "fr", "es"],
117
118
119
        packages=[],
        admin_account=None,
        root_partition_size=root_partition_size,
120
121
        disk_size=disk_size,
    )
thiolliere's avatar
thiolliere committed
122

123
    run(machine, tags, extra_vars)
thiolliere's avatar
thiolliere committed
124

125

126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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 """
143
144

    extra_vars = {
145
146
        "root_partition_size": root_partition_size,
        "disk_size": disk_size,
147
148
149
150
151
152
153
154
155
156
157
158
159
        "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",
160
161
162
    }

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

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

    return extra_vars, secret_keys


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

182
    tags = ["resize", "rename", "reconfigure"]
183
184

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

    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))

192
    extra_vars.update({"has_custom_branding": has_custom_branding})
193

194
195
    # save YAML catalogs into local files inside VM for use by ideascube
    for index, catalog in enumerate(CATALOGS):
196
        with tempfile.NamedTemporaryFile(suffix=".yml", delete=False) as fd:
197
198
199
200
201
202
203
204
            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://", ""))
205
            os.unlink(fd.name)
206

207
208
209
210
    run(machine, tags, extra_vars, secret_keys)


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

213
    tags = ["move-content", "seal"]
214

215
    run(machine, tags, extra_vars, secret_keys)