blob: 4a0671e8a3ea41addbe59f223c53fb2394c30719 [file] [log] [blame]
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +01001#!/usr/bin/env python3
2#
3# Generate config drives v2 for MCP instances.
4#
5# Config Drive v2 links:
6# - structure: https://cloudinit.readthedocs.io/en/latest/topics/datasources/configdrive.html#version-2
7# - network configuration: https://cloudinit.readthedocs.io/en/latest/topics/network-config.html
8#
9# This script uses OpenStack Metadata Service Network format for configuring networking.
10#
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +000011__author__ = "Dzmitry Stremkouski"
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +010012__copyright__ = "Copyright 2019, Mirantis Inc."
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +000013__license__ = "Apache 2.0"
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +010014
15import argparse
16from crypt import crypt
17from json import dump as json_dump
18from os import makedirs, umask
19from shutil import copytree, copyfile, copyfileobj, rmtree
20from subprocess import call as run
21from sys import argv, exit
22from uuid import uuid1 as uuidgen
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +000023from yaml import safe_load
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +010024
25
26def crash_with_error(msg, exit_code=1):
27 print("ERROR: \n" + msg)
28 exit(exit_code)
29
30
31def xprint(msg):
32 if not args.quiet:
33 print(msg)
34
35
36def calculate_hostnames(name, hostname):
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +000037
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +010038 if len(name.split('.')) > 1:
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +000039 crash_with_error("instance name should be in short format without domain")
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +010040 else:
41 if len(hostname.split('.')) == 1:
42 if not name == uuid:
43 hostname = name
44 else:
45 name = hostname
46 else:
47 if name == uuid:
48 name = hostname.split('.')[0]
49
50 return [name, hostname]
51
52
53def validate_args(args):
54 if not args.user_data:
55 if args.cloud_user_name or args.cloud_user_pass:
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +000056 crash_with_error("You have not specified user-data file path, but require cloud-user setup, which requires it.")
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +010057
58 if not args.skip_network and not args.network_data:
59 if not args.ip or not args.netmask or not args.interface:
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +000060 crash_with_error("You have not specified neither ip nor netmask nor interface nor network_data.json file.\nEither skip network configuration or provide network_data.json file path.")
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +010061
62 if args.skip_network and args.network_data:
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +000063 crash_with_error("--skip-network and --network-data are mutually exclusive.")
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +010064
65
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +000066def generate_iso(cfg_file_path, cfg_dir_path, quiet = ''):
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +010067 xprint("Generating config drive image: %s" % cfg_file_path)
68 cmd = ["mkisofs", "-r", "-J", "-V", "config-2", "-input-charset", "utf-8"]
69 if quiet:
70 cmd.append("-quiet")
71 cmd += ["-o", cfg_file_path, cfg_dir_path]
72 run(cmd)
73
74
75def create_config_drive(args):
76 name, hostname = calculate_hostnames(args.name, args.hostname)
77 username = args.cloud_user_name
78 if args.cloud_user_pass:
79 userpass = args.cloud_user_pass
80 else:
81 userpass = ""
82
83 cfg_file_path = hostname + '-config.iso'
84 cfg_dir_path = '/var/tmp/config-drive-' + uuid
85 mcp_dir_path = cfg_dir_path + '/mcp'
86 model_path = mcp_dir_path + '/model'
87 mk_pipelines_path = mcp_dir_path + '/mk-pipelines'
88 pipeline_lib_path = mcp_dir_path + '/pipeline-library'
89 meta_dir_path = cfg_dir_path + '/openstack/latest'
90 meta_file_path = meta_dir_path + '/meta_data.json'
91 user_file_path = meta_dir_path + '/user_data'
92 net_file_path = meta_dir_path + '/network_data.json'
93 vendor_file_path = meta_dir_path + '/vendor_data.json'
94 gpg_file_path = mcp_dir_path + '/gpg'
95
96 umask(0o0027)
97 makedirs(mcp_dir_path)
98 makedirs(meta_dir_path)
99
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +0000100 meta_data = {}
101 meta_data["uuid"] = uuid
102 meta_data["hostname"] = hostname
103 meta_data["name"] = name
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +0100104 network_data = {}
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +0000105 gateway_ip = ""
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +0100106
107 ssh_keys = []
108
109 if args.ssh_key:
110 xprint("Adding authorized key to config drive: %s" % str(args.ssh_key))
111 ssh_keys.append(args.ssh_key)
112
113 if args.ssh_keys:
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +0000114 xprint("Adding authorized keys file entries to config drive: %s" % str(args.ssh_keys))
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +0100115 with open(args.ssh_keys, 'r') as ssh_keys_file:
116 ssh_keys += ssh_keys_file.readlines()
117 ssh_keys = [x.strip() for x in ssh_keys]
118
119 # Deduplicate keys if any
120 ssh_keys = list(set(ssh_keys))
121
122 # Load keys
123 if len(ssh_keys) > 0:
124 meta_data["public_keys"] = {}
125 for i in range(len(ssh_keys)):
126 meta_data["public_keys"][str(i)] = ssh_keys[i]
127
128 if args.model:
129 xprint("Adding cluster model to config drive: %s" % str(args.model))
130 copytree(args.model, model_path)
131
132 if args.pipeline_library:
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +0000133 xprint("Adding pipeline-library to config drive: %s" % str(args.pipeline_library))
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +0100134 copytree(args.pipeline_library, pipeline_lib_path)
135
136 if args.mk_pipelines:
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +0000137 xprint("Adding mk-pipelines to config drive: %s" % str(args.mk_pipelines))
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +0100138 copytree(args.mk_pipelines, mk_pipelines_path)
139
140 if args.gpg_key:
141 xprint("Adding gpg keys file to config drive: %s" % str(args.gpg_key))
142 makedirs(gpg_file_path)
143 copyfile(args.gpg_key, gpg_file_path + '/salt_master_pillar.asc')
144
145 if args.vendor_data:
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +0000146 xprint("Adding vendor metadata file to config drive: %s" % str(args.vendor_data))
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +0100147 copyfile(args.vendor_data, vendor_file_path)
148
149 with open(meta_file_path, 'w') as meta_file:
150 json_dump(meta_data, meta_file)
151
152 if args.user_data:
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +0000153 xprint("Adding user data file to config drive: %s" % str(args.user_data))
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +0100154 if username:
155 with open(user_file_path, 'a') as user_file:
156 users_data = "#cloud-config\n"
Denis Egorenkob1d3c202019-03-20 18:03:22 +0400157 users_data += "users:\n"
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +0100158 users_data += " - name: %s\n" % username
159 users_data += " sudo: ALL=(ALL) NOPASSWD:ALL\n"
160 users_data += " groups: admin\n"
161 users_data += " lock_passwd: false\n"
162 if userpass:
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +0000163 users_data += " passwd: %s\n" % str(crypt(userpass, '$6$'))
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +0100164 if ssh_keys:
165 users_data += " ssh_authorized_keys:\n"
166 for ssh_key in ssh_keys:
167 users_data += " - %s\n" % ssh_key
168 users_data += "\n"
169 user_file.write(users_data)
170 with open(args.user_data, 'r') as user_data_file:
171 copyfileobj(user_data_file, user_file)
172 else:
173 copyfile(args.user_data, user_file_path)
174
175 if args.network_data:
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +0000176 xprint("Adding network metadata file to config drive: %s" % str(args.network_data))
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +0100177 copyfile(args.network_data, net_file_path)
178 else:
179 if not args.skip_network:
180 xprint("Configuring network metadata from specified parameters.")
181 network_data["links"] = []
182 network_data["networks"] = []
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +0000183 network_data["links"].append({"type": "phy", "id": args.interface, "name": args.interface})
184 network_data["networks"].append({"type": "ipv4", "netmask": args.netmask, "link": args.interface, "id": "private-ipv4", "ip_address": args.ip})
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +0100185 if args.dns_nameservers:
186 network_data["services"] = []
187 for nameserver in args.dns_nameservers.split(','):
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +0000188 network_data["services"].append({"type": "dns", "address": nameserver})
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +0100189 if args.gateway:
190 network_data["networks"][0]["routes"] = []
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +0000191 network_data["networks"][0]["routes"].append({"netmask": "0.0.0.0", "gateway": args.gateway, "network": "0.0.0.0"})
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +0100192
193 # Check if network metadata is not skipped
194 if len(network_data) > 0:
195 with open(net_file_path, 'w') as net_file:
196 json_dump(network_data, net_file)
197
198 generate_iso(cfg_file_path, cfg_dir_path, args.quiet)
199 if args.clean_up:
200 xprint("Cleaning up working dir.")
201 rmtree(cfg_dir_path)
202
203
204if __name__ == '__main__':
205 uuid = str(uuidgen())
Aleksey Zvyagintsev5c397f52019-04-04 14:15:47 +0000206 parser = argparse.ArgumentParser(description='Config drive generator for MCP instances.', prog=argv[0], usage='%(prog)s [options]')
207 parser.add_argument('--gpg-key', type=str, help='Upload gpg key for salt master. Specify path to file in asc format.', required=False)
208 parser.add_argument('--name', type=str, default=uuid, help='Specify instance name. Hostname in short format, without domain.', required=False)
209 parser.add_argument('--hostname', type=str, default=uuid, help='Specify instance hostname. FQDN. Hostname in full format with domain. Shortname would be trated as name.', required=False)
210 parser.add_argument('--skip-network', action='store_true', help='Do not generate network_data for the instance.', required=False)
211 parser.add_argument('--interface', type=str, default='ens3', help='Specify interface for instance to configure.', required=False)
212 parser.add_argument('--ssh-key', type=str, help='Specify ssh public key to upload to cloud image.', required=False)
213 parser.add_argument('--ssh-keys', type=str, help='Upload authorized_keys to cloud image. Specify path to file in authorized_keys format.', required=False)
214 parser.add_argument('--cloud-user-name', type=str, help='Specify cloud user name.', required=False)
215 parser.add_argument('--cloud-user-pass', type=str, help='Specify cloud user password.', required=False)
216 parser.add_argument('--ip', type=str, help='Specify IP address for instance.', required=False)
217 parser.add_argument('--netmask', type=str, help='Specify netmask for instance.', required=False)
218 parser.add_argument('--gateway', type=str, help='Specify gateway address for instance.', required=False)
219 parser.add_argument('--dns-nameservers', type=str, help='Specify DNS nameservers delimited by comma.', required=False)
220 parser.add_argument('--user-data', type=str, help='Specify path to user_data file in yaml format.', required=False)
221 parser.add_argument('--vendor-data', type=str, help='Specify path to vendor_data.json in openstack vendor metadata format.', required=False)
222 parser.add_argument('--network-data', type=str, help='Specify path to network_data.json in openstack network metadata format.', required=False)
223 parser.add_argument('--model', type=str, help='Specify path to cluster model.', required=False)
224 parser.add_argument('--mk-pipelines', type=str, help='Specify path to mk-pipelines folder.', required=False)
225 parser.add_argument('--pipeline-library', type=str, help='Specify path to pipeline-library folder.', required=False)
226 parser.add_argument('--clean-up', action='store_true', help='Clean-up config-drive dir once ISO is created.', required=False)
227 parser.add_argument('--quiet', action='store_true', help='Keep silence. Do not write any output messages to stout.', required=False)
Dzmitry Stremkouski9dd6a1b2019-01-24 12:03:58 +0100228 args = parser.parse_args()
229
230 if len(argv) < 2:
231 parser.print_help()
232 exit(0)
233
234 validate_args(args)
235 create_config_drive(args)