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#
11__author__ = "Dzmitry Stremkouski"
12__copyright__ = "Copyright 2019, Mirantis Inc."
13__license__ = "Apache 2.0"
14
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
23from yaml import safe_load
24
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):
37
38 if len(name.split('.')) > 1:
39 crash_with_error("instance name should be in short format without domain")
40 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:
56 crash_with_error("You have not specified user-data file path, but require cloud-user setup, which requires it.")
57
58 if not args.skip_network and not args.network_data:
59 if not args.ip or not args.netmask or not args.interface:
60 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.")
61
62 if args.skip_network and args.network_data:
63 crash_with_error("--skip-network and --network-data are mutually exclusive.")
64
65
66def generate_iso(cfg_file_path, cfg_dir_path, quiet = ''):
67 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
100 meta_data = {}
101 meta_data["uuid"] = uuid
102 meta_data["hostname"] = hostname
103 meta_data["name"] = name
104 network_data = {}
105 gateway_ip = ""
106
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:
114 xprint("Adding authorized keys file entries to config drive: %s" % str(args.ssh_keys))
115 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:
133 xprint("Adding pipeline-library to config drive: %s" % str(args.pipeline_library))
134 copytree(args.pipeline_library, pipeline_lib_path)
135
136 if args.mk_pipelines:
137 xprint("Adding mk-pipelines to config drive: %s" % str(args.mk_pipelines))
138 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:
146 xprint("Adding vendor metadata file to config drive: %s" % str(args.vendor_data))
147 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:
153 xprint("Adding user data file to config drive: %s" % str(args.user_data))
154 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:
163 users_data += " passwd: %s\n" % str(crypt(userpass, '$6$'))
164 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:
176 xprint("Adding network metadata file to config drive: %s" % str(args.network_data))
177 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"] = []
183 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})
185 if args.dns_nameservers:
186 network_data["services"] = []
187 for nameserver in args.dns_nameservers.split(','):
188 network_data["services"].append({"type": "dns", "address": nameserver})
189 if args.gateway:
190 network_data["networks"][0]["routes"] = []
191 network_data["networks"][0]["routes"].append({"netmask": "0.0.0.0", "gateway": args.gateway, "network": "0.0.0.0"})
192
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())
206 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)
228 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)