Browse Source

Initial commit

Sebastian Lohff 3 years ago
commit
79110a528c
1 changed files with 317 additions and 0 deletions
  1. 317
    0
      genconfdrv

+ 317
- 0
genconfdrv View File

@@ -0,0 +1,317 @@
1
+#!/usr/bin/env python3
2
+# -*- coding: utf-8 -*-
3
+from __future__ import print_function
4
+
5
+import argparse
6
+#import fs
7
+#from fs import tempfs, path
8
+import fs.tempfs
9
+import fs.path
10
+import ipaddress
11
+import json
12
+import os
13
+import subprocess
14
+import sys
15
+import uuid
16
+
17
+__VERSION__ = '0.1'
18
+
19
+
20
+class ConfigDrive:
21
+    def __init__(self, genisoimage='/usr/bin/genisoimage', verbose=False):
22
+        self._tmpfs = None
23
+        self._genisoimage = genisoimage
24
+        self._user_data = {}  # {"system_info": {"default_user": None}}
25
+        self._interfaces = []
26
+        self._pubkeys = []
27
+        self._verbose = verbose
28
+
29
+        self._added_resolv_module_call = False
30
+
31
+        if not os.path.exists(genisoimage):
32
+            print("Error: %s does not exist, no genisoimage found!" % genisoimage, file=sys.stderr)
33
+            sys.exit(1)
34
+
35
+        self.open()
36
+
37
+    def set_hostname(self, hostname):
38
+        self._hostname = hostname
39
+
40
+    def conf_network(self, interface, address=None, gateway=None):
41
+        if not address and gateway:
42
+            raise ValueError("You cannot define a gateway, but supply no address")
43
+
44
+        if not self._interfaces:
45
+            self._interfaces.extend([
46
+                "auto lo",
47
+                "iface lo inet loopback",
48
+            ])
49
+
50
+        if address:
51
+            if "/" not in address:
52
+                raise ValueError("IP Interface is not a subnet")
53
+            address = ipaddress.ip_interface(address)
54
+            method = "static"
55
+        else:
56
+            method = "dhcp"
57
+
58
+        self._interfaces.extend([
59
+            "",
60
+            "auto %s" % interface,
61
+            "iface %s inet%s %s" % (interface, "" if not address or address.version == 4 else "6", method),
62
+        ])
63
+
64
+        if address:
65
+            self._interfaces.append("    address %s" % address)
66
+
67
+        if gateway:
68
+            self._interfaces.append("    gateway %s" % str(ipaddress.ip_address(gateway)))
69
+
70
+    def conf_resolve(self, resolvers):
71
+        if not self._hostname:
72
+            raise ValueError("Please set a hostname before calling this function")
73
+
74
+        for n, resolver in enumerate(resolvers, 1):
75
+            try:
76
+                ipaddress.ip_address(resolver)
77
+            except ValueError as e:
78
+                print("Nameserver argument %s: %s" % (n, e), file=sys.stderr)
79
+                sys.exit(1)
80
+
81
+        self._user_data["manage_resolv_conf"] = True
82
+        self._user_data["resolv_conf"] = {
83
+            "nameservers": resolvers,
84
+        }
85
+
86
+        if "." in self._hostname:
87
+            self._user_data["domain"] = ".".join(self._hostname.split(".")[1:])
88
+
89
+        # debian, by default, does not call the cc_resolv_conf module
90
+
91
+        if not self._added_resolv_module_call:
92
+            self.add_command("cloud-init single --name cc_resolv_conf", True)
93
+            self._added_resolv_module_call = True
94
+
95
+    def add_user(self, name, keys=None, gecos=None, sudo=False, password=None):
96
+        if "users" not in self._user_data:
97
+            self._user_data["users"] = []
98
+
99
+        user = {
100
+            "name": name,
101
+            "shell": "/bin/bash",
102
+            "home": "/home/%s" % name
103
+        }
104
+
105
+        if keys:
106
+            if type(keys) == str:
107
+                keys = [keys]
108
+
109
+            user["ssh_authorized_keys"] = keys
110
+
111
+        if gecos:
112
+            user["gecos"] = gecos
113
+
114
+        if sudo:
115
+            user["sudo"] = "ALL=(ALL) NOPASSWD:ALL"
116
+
117
+        if password:
118
+            raise NotImplementedError("crypt, salt, $6$ something")
119
+
120
+        self._user_data["users"].append(user)
121
+
122
+    def add_fp(self, path, fp):
123
+        self.add_text(path, fp.read())
124
+
125
+    def add_text(self, path, content):
126
+        dir_path = fs.path.dirname(path)
127
+        if dir_path and not self._tmpfs.exists(dir_path):
128
+            self._tmpfs.makedirs(dir_path)
129
+
130
+
131
+        self._tmpfs.settext(path, content)
132
+        if self._verbose:
133
+            print(" >>", path)
134
+            print(content)
135
+            print()
136
+
137
+    def open(self):
138
+        if not self._tmpfs:
139
+            self._tmpfs = fs.tempfs.TempFS("genconfdrv", auto_clean=True)
140
+
141
+    def _write_metadata(self):
142
+        if not self._hostname:
143
+            raise ValueError("No hostname set")
144
+
145
+        meta_data = {
146
+            # "availability_zone": "cat",
147
+            "files": [],
148
+            "hostname": self._hostname,
149
+            "name": self._hostname.split(".")[0],
150
+            #"meta": {
151
+            #    "role": "webservers",
152
+            #    "essential": False,
153
+            #}
154
+            "uuid": str(uuid.uuid4()),
155
+        }
156
+
157
+        if self._interfaces:
158
+            # add the source-directory to interfaces
159
+            self._interfaces.extend([
160
+                "",
161
+                "source-directory /etc/network/interfaces.d/",
162
+                "",
163
+            ])
164
+
165
+            meta_data["files"].append({"content_path": "/content/0000", "path": "/etc/network/interfaces"})
166
+            self.add_text("/openstack/content/0000", "\n".join(self._interfaces))
167
+
168
+            meta_data["files"].append({"content_path": "/content/0001", "path": "/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg"})
169
+            self.add_text("/openstack/content/0001", "network: {config: disabled}")
170
+
171
+        #meta_data["files"].append({"content_path": "/content/0002", "path": "/etc/cloud/cloud.cfg.d/10-resolv-conf.cfg"})
172
+        #self.add_text("/openstack/content/0002", "cloud_init_modules:\n  - resolv-conf\n")
173
+
174
+        if self._pubkeys:
175
+            meta_data["public_keys"] = {}
176
+            for n, key in enumerate(self._pubkeys):
177
+                meta_data["public_keys"]["key-%02d" % n] = key
178
+
179
+        self.add_text("/openstack/latest/meta_data.json", json.dumps(meta_data, indent=4))
180
+
181
+    def enable_upgrades(self):
182
+        self._user_data["package_update"] = True
183
+        self._user_data["package_upgrade"] = True
184
+
185
+    def add_command(self, command, boot=True):
186
+        key = "bootcmd" if boot else "runcmd"
187
+        if key not in self._user_data:
188
+            self._user_data[key] = []
189
+
190
+        self._user_data[key].append(command)
191
+
192
+    def add_pubkey(self, pubkey):
193
+        self._pubkeys.append(pubkey)
194
+
195
+    def set_password(self, user, password):
196
+        if "chpasswd" not in self._user_data:
197
+            self._user_data["chpasswd"] = {}
198
+            self._user_data["chpasswd"]["list"] = ""
199
+            #self._user_data["chpasswd"]["list"] = []
200
+
201
+        #self._user_data["chpasswd"]["list"].append("%s:%s" % (user, password))
202
+        self._user_data["chpasswd"]["list"] += "%s:%s\n" % (user, password)
203
+
204
+    def _write_userdata(self):
205
+        self.add_text("/openstack/latest/user_data", "#cloud-config\n" + json.dumps(self._user_data, indent=4))
206
+
207
+    def write_iso(self, path):
208
+        self._write_metadata()
209
+        self._write_userdata()
210
+
211
+        p = subprocess.Popen([self._genisoimage,
212
+                              "-J", "-r", "-q",
213
+                              "-V", "config-2",
214
+                              "-publisher", "seba-genconfdrv"
215
+                              "-l", "-ldots", "-allow-lowercase", "-allow-multidot",
216
+                              "-input-charset", "utf-8",
217
+                              "-o", path,
218
+                              self._tmpfs.getsyspath(""),
219
+                              ])
220
+
221
+        p.wait()
222
+
223
+    def close(self):
224
+        if self._tmpfs:
225
+            self._tmpfs.close()
226
+
227
+# defaults for testing
228
+#        cfgdrv.set_hostname("foo.someserver.de")
229
+#        cfgdrv.conf_network("ens3", "172.23.0.4/24", "172.23.0.1")
230
+#        cfgdrv.conf_resolve(["1.1.1.1", "8.8.8.8"])
231
+#        cfgdrv.enable_upgrades()
232
+#        cfgdrv.add_command("rm -rf /home/debian/; userdel debian; groupdel debian", True)
233
+#        cfgdrv.add_command("cloud-init single --name cc_resolv_conf", True)
234
+#        cfgdrv.add_command("rm -f /etc/network/interfaces.d/eth*.cfg", True)
235
+#        cfgdrv.add_command("sed -rni '/^([^#]|## template)/p' /etc/cloud/templates/sources.list.*.tmpl; rm /etc/apt/sources.list.d/*", True)
236
+#        #cfgdrv.add_command("(whoami; date) > /root/bleep", False)
237
+#        cfgdrv.add_pubkey("ssh-rsa bleep foo")
238
+#        cfgdrv.set_password("root", "kitteh")
239
+
240
+
241
+def main():
242
+    parser = argparse.ArgumentParser()
243
+
244
+    parser.add_argument("-H", "--hostname", required=True, help="Hostname")
245
+    parser.add_argument("-o", "--output", required=True, help="Path to write iso to")
246
+    parser.add_argument("-n", "--nameservers", "--ns", default=["1.1.1.1", "8.8.8.8"], nargs="+", help="Nameservers")
247
+    parser.add_argument("-i", "--networks", "--net", default=[], nargs="+")
248
+    parser.add_argument("-u", "--disable-upgrades", action="store_true", default=False)
249
+    parser.add_argument("-v", "--verbose", action="store_true", default=False)
250
+    parser.add_argument("--no-debian-cleanup", "--ndc", action="store_true", default=False)
251
+    parser.add_argument("--set-root-password", "--srp", default=None)
252
+    parser.add_argument("-a", "--add-user", default=[], nargs="+", help="Add users, format is username:key?:sudo?:gecos?:password?, sudo is a bool, key is either an ssh key or a path to an ssh key")
253
+
254
+
255
+    args = parser.parse_args()
256
+
257
+    cfgdrv = None
258
+    try:
259
+        cfgdrv = ConfigDrive(verbose=args.verbose)
260
+
261
+        cfgdrv.set_hostname(args.hostname)
262
+
263
+        for net in args.networks:
264
+            net = net.split(":")
265
+            cfgdrv.conf_network(*net)
266
+
267
+        if args.nameservers:
268
+            cfgdrv.conf_resolve(args.nameservers)
269
+
270
+        if not args.disable_upgrades:
271
+            cfgdrv.enable_upgrades()
272
+
273
+        if not args.no_debian_cleanup:
274
+            cfgdrv.add_command("rm -f /etc/network/interfaces.d/eth*.cfg", True)
275
+            cfgdrv.add_command("sed -rni '/^([^#]|## template)/p' /etc/cloud/templates/sources.list.*.tmpl; rm /etc/apt/sources.list.d/*", True)
276
+            cfgdrv.add_command("sed -rni '/^([^#]|## template)/p' /etc/resolv.conf /etc/cloud/templates/resolv.conf.tmpl", True)
277
+
278
+
279
+        if args.set_root_password:
280
+            cfgdrv.set_password("root", args.set_root_password)
281
+
282
+        if args.add_user:
283
+            for user in args.add_user:
284
+                user = user.split(":")
285
+                # user key sudo gecos password
286
+                if len(user) < 2:
287
+                    parser.error("Missing key parameter for user")
288
+
289
+                keys = ""
290
+                if len(user) >= 2:
291
+                    if user[1].startswith("ssh-"):
292
+                        keys = [user[1]]
293
+                    else:
294
+                        with open(os.path.expanduser(user[1])) as keyfile:
295
+                            keys = keyfile.read().split("\n")
296
+
297
+                sudo = True
298
+                if len(user) >= 3:
299
+                    sudo = user[2] not in (False, 0, "0", "no", "false", "False")
300
+
301
+                gecos = None
302
+                if len(user) >= 4:
303
+                    gecos = user[3]
304
+
305
+                password = None
306
+                if len(user) >= 5:
307
+                    password = user[4]
308
+                cfgdrv.add_user(user[0], keys, sudo=sudo, gecos=gecos, password=password)
309
+
310
+        if args.output:
311
+            cfgdrv.write_iso(args.output)
312
+    finally:
313
+        if cfgdrv:
314
+            cfgdrv.close()
315
+
316
+if __name__ == '__main__':
317
+    main()

Loading…
Cancel
Save