def update_password(iso_name): users_file = pathlib.Path(constants.IMAGE_FOLDER) / iso_name / constants.SETUP_USERS_FILE with open(users_file) as fh: curr_users = fh.read() db = get_db() passwords = db.execute('SELECT username, password_hash FROM passwords').fetchall() pass_dict = {} for password in passwords: pass_dict[password[0]] = password[1] new_user_lines = [] for line in curr_users.split('\n'): if re.match('^rootpw .+$', line): try: phash = pass_dict['root'] line = re.sub(r'--iscrypted .+', f'--iscrypted {phash}', line) except KeyError: pass elif re.match('^user .+$', line): username = re.match('^user.* --name=([a-z0-9_-]+) .*$', line).group(1) try: phash = pass_dict[username] line = re.sub(r'--password=[aA-zZ0-9$+/.]+ ', f'--password={phash} ', line) except KeyError: pass new_user_lines.append(line) with open(users_file, 'w') as fh: fh.write('\n'.join(new_user_lines))
def fetch(): db = get_db() blasted_nodes = db.execute( 'SELECT id, identifier FROM node WHERE status = "Blasted" and quickstart_id is null' ).fetchall() blasted_list = [] for node in blasted_nodes: blasted_list.append(node[1]) conductors = db.execute( 'SELECT name, url, auth_key FROM conductor').fetchall() errors = [] for conductor in conductors: nodes, error = get_all_nodes(conductor[0], conductor[1], conductor[2]) if error: errors.append(error) continue for node in nodes: if node['assetId'] in blasted_list: router_name = node['router']['name'] node_name = node['name'] assetId = node['assetId'] success, qs_id_or_error = get_quickstart_work( conductor[0], router_name, node_name, assetId) if success: associate_quickstart_to_node(qs_id_or_error, assetId) else: errors.append(qs_id_or_error) flash('\n'.join(errors)) return redirect(url_for('node.list'))
def download_image(name): print(f"Adding ISO {name} to DB") db = get_db() db.execute('INSERT INTO iso (name, status) VALUES (?, ?)', (name, 'Processing')) db.commit() # ensure iso folder exists try: os.makedirs(constants.IMAGE_FOLDER) except OSError: pass print(f"Attempting to download ISO {name}") try: with requests.get(f"{constants.ISO_REPO_URL}{name}.iso", cert=os.path.join(constants.CERTIFICATE_FOLDER, constants.CERT_FILE), allow_redirects=True, stream=True) as dl: dl.raise_for_status() with open(iso_file(name), 'wb+') as iso: for chunk in dl.iter_content(chunk_size=1024000): # 1M chunks iso.write(chunk) except Exception as e: print(f"Exception downloading ISO {name}: {e}") update_db_failed(name) return False print(f"ISO {name} downloaded successfully") return stage_image(name)
def update_isos(): db = get_db() isos = db.execute('SELECT name FROM iso').fetchall() for iso in isos: update_password(iso[0]) flash(f"Passwords updated for ISO {iso[0]}") return redirect(url_for('password.menu'))
def upload(): if request.method == 'POST': if 'file' not in request.files: flash('No file part') return redirect(request.url) file = request.files['file'] if file.filename == '': flash('No file selected') return redirect(request.url) if file: try: os.mkdir(constants.IMAGE_FOLDER) except OSError: pass file.save(pathlib.Path(constants.IMAGE_FOLDER) / file.filename) name = os.path.splitext(file.filename)[0] db = get_db() db.execute('INSERT INTO iso (name, status) VALUES (?, ?)', (name, 'Processing')) db.commit() stage_image.delay(name) return redirect(url_for('menu.home')) return render_template('iso_upload.html')
def instantiate(instance=None): db = get_db() node_row = db.execute( 'SELECT quickstart_id from node WHERE identifier = ?', (instance, )).fetchone() if node_row is None: qs_row = db.execute( 'SELECT node_name, asset_id, config FROM quickstart WHERE default_quickstart > 0' ).fetchone() else: qs_row = db.execute( 'SELECT node_name, asset_id, config FROM quickstart WHERE id = ?', (node_row['quickstart_id'], )).fetchone() if qs_row is None: return jsonify( error="Could not find a specific or default quickstart"), 404 response = {} quickstart = { 'a': qs_row['asset_id'], 'n': qs_row['node_name'], 'c': qs_row['config'] } response['quickstart'] = json.dumps(quickstart) response['password'] = None db.execute('UPDATE node SET status = ? WHERE identifier = ?', ('Bootstrapped', instance)) db.commit() return jsonify(response)
def delete(id=None): if id is None: flash("No id specificed for node add action") return redirect(url_for('node.menu')) db = get_db() db.execute('DELETE FROM node WHERE identifier = ?', (id, )) db.commit() return redirect(url_for('node.list'))
def remove_asset(id=None): if id is None: flash("No quickstart specified for set as default action") return redirect(url_for('manage_quickstart.list')) db = get_db() db.execute('UPDATE quickstart SET asset_id = NULL WHERE id = ?', (id,)) db.commit() flash("Removed asset_id from quickstart") return redirect(url_for('manage_quickstart.menu'))
def delete(id=None): if id is None: flash("No quickstart specificed for delete action") return redirect(url_for('manage_quickstart.menu')) db = get_db() db.execute('DELETE FROM quickstart where id = ?', (id,)) db.commit() flash(f"Deleted quickstart") return redirect(url_for('manage_quickstart.list'))
def list(): db = get_db() nodes = db.execute( 'select n.id, n.identifier, n.status, n.iso_id, q.conductor_name, q.router_name, q.node_name, q.asset_id FROM node n LEFT JOIN quickstart q ON n.quickstart_id = q.id' ).fetchall() quickstarts = db.execute('select * FROM quickstart').fetchall() print(nodes) return render_template('node_list.html', nodes=nodes, quickstarts=quickstarts)
def delete(name=None): if name is None: flash("No Conductor specificed for delete action") return redirect(url_for('conductor.menu')) db = get_db() db.execute('DELETE FROM conductor WHERE name = ?', (name,)) db.commit() flash(f"Deleted Conductor {name}") return redirect(url_for('conductor.list'))
def delete(username=None): if username is None: flash("No username specified for password delete action") return redirect(url_for('password.menu')) db = get_db() db.execute('DELETE FROM passwords WHERE username = ?', (username,)) db.commit() flash(f"Password entry for {username} updated") return redirect(url_for('password.menu'))
def set_as_default(id=None): if id is None: flash("No quickstart specified for set as default action") return redirect(url_for('manage_quickstart.list')) clear_default_qs() db = get_db() db.execute('UPDATE quickstart SET default_quickstart = 1 WHERE id = ?', (id,)) db.commit() flash("Updated default quickstart") return redirect(url_for('manage_quickstart.menu'))
def remove_image(name): print(f"Removing ISO {name} from DB") db = get_db() try: os.remove(f"{constants.IMAGE_FOLDER}/{name}.iso") print(f"ISO file {name} removed") except (OSError, FileNotFoundError): print(f"Error removing ISO file {name}") try: shutil.rmtree(f"{pathlib.Path(constants.IMAGE_FOLDER) / name}") print(f"Removed NFS share {name}") except (OSError, FileNotFoundError): print(f"Error removing NFS share {name}") try: shutil.rmtree( pathlib.Path(constants.UEFI_TFTPBOOT_DIR) / "images" / name) print(f"Removed UEFI TFTP image for {name}") except (OSError, FileNotFoundError): print(f"Error removing UEFI TFTP image for {name}") try: shutil.rmtree( pathlib.Path(constants.BIOS_TFTPBOOT_DIR) / "images" / name) print(f"Removed BIOS TFTP image for {name}") except (OSError, FileNotFoundError): print(f"Error removing BIOS TFTP image for {name}") try: os.system(f"sed -i '/{name}/d' /etc/exports") os.system('exportfs -ra') print(f"Removed NFS share for {name} from exports") except Error: print(f"Error removing NFS share for {name} from exports file") try: os.remove( pathlib.Path(constants.UEFI_TFTPBOOT_DIR) / "pxelinux.cfg" / name) print(f"Removed UEFI pxelinux config for {name}") except (OSError, FileNotFoundError): print(f"Error removing UEFI pxelinux config for {name}") try: os.remove( pathlib.Path(constants.BIOS_TFTPBOOT_DIR) / "pxelinux.cfg" / name) print(f"Removed BIOS pxelinux config for {name}") except (OSError, FileNotFoundError): print(f"Error removing BIOS pxelinux config for {name}") db.execute('DELETE FROM iso WHERE name = ?', (name, )) db.commit()
def get_quickstart_work(conductor, router, node, assetId): db = get_db() conductor_data = db.execute('SELECT url, auth_key FROM conductor WHERE name = ?', (conductor,)).fetchone() token = conductor_data['auth_key'] qs_url = conductor_data['url'] + '/api/v1/quickStart' headers = { 'Content-Type': 'application/json', 'Authorization': f"Bearer {token}" } node_data = { 'router': router, 'node': node, 'assetId': assetId, 'password': None } try: qs_resp = requests.post(qs_url, headers=headers, data=json.dumps(node_data), verify=False) except requests.exceptions.Timeout: error = f"Timeout connecting to conductor {name}" return False, error if not qs_resp.ok: error = f"Conductor {name} returned error for quickstart query {qs_resp.status_code}: {qs_resp.json()}" return False, error try: qs_json = qs_resp.json() qs_node = qs_json['n'] qs_asset = qs_json['a'] qs_config = qs_json['c'] cursor = db.cursor() cursor.execute('INSERT INTO quickstart (' \ 'conductor_name, ' \ 'router_name, ' \ 'node_name, ' \ 'asset_id, ' \ 'config, ' \ 'description) VALUES (?, ?, ?, ?, ?, ?)', (conductor, router, qs_node, qs_asset, qs_config, 'Retrieved by blaster')) qs_id = cursor.lastrowid db.commit() except KeyError: error = f"Error parsing quickstart data returned from conductor {conductor} for router {router} node {node}" return False, error return True, qs_id
def add(id=None): if id is None: flash("No id specificed for node add action") return redirect(url_for('node.menu')) db = get_db() active_name = get_active() # Avoid duplicates. Assume reblasted, clear out old entry and add a new one db.execute('DELETE FROM node WHERE identifier = ?', (id, )) db.execute( 'INSERT INTO node (identifier, iso_id, status) VALUES (?, ?, ?)', (id, active_name, 'Blasted')) db.commit() return jsonify(added=id, blasted_with=active_name), 200
def list_nodes(name=None): if name is None: flash("No Conductor specificed for delete action") return redirect(url_for('conductor.menu')) db = get_db() db_res = db.execute('SELECT url, auth_key FROM conductor WHERE name = ?', (name,)).fetchone() nodes, error = get_all_nodes(name, db_res[0], db_res[1]) print("Test123") print(nodes) if nodes: return render_template('conductor_list_nodes.html', nodes=nodes, conductor=name) flash(error) return redirect(url_for('conductor.menu'))
def add(name=None): db = get_db() if request.method == 'POST': if 'file' not in request.files: flash('No file part') return redirect(request.url) router = request.form.get('router') conductor = request.form.get('conductor') description = request.form.get('description') # Not supported yet #password = request.form.get('password') file = request.files['file'] try: qs_contents = file.read() except UnicodeDecodeError: flash("Quickstart file appears to be encrypted, which is not supported at this time") return redirect(url_for('manage_quickstart.menu')) try: qs_data = json.loads(qs_contents) except json.decoder.JSONDecodeError: flash("Quickstart does not appear to have valid JSON, please check contents") return redirect(url_for('manage_quickstart.menu')) node = qs_data.get('n') asset = qs_data.get('a') config = qs_data.get('c') db.execute('INSERT INTO quickstart (' \ 'conductor_name, ' \ 'router_name, ' \ 'node_name, ' \ 'asset_id, ' \ 'config, ' \ 'description) VALUES (?, ?, ?, ?, ?, ?)', (conductor, router, node, asset, config, description)) db.commit() flash("Quickstart uploaded successfully") return redirect(url_for('manage_quickstart.menu')) elif request.method == 'GET': return render_template('quickstart_add.html')
def add(name=None): if name is None: flash("No image specificed for add action") return redirect(url_for('iso.menu')) db = get_db() if db.execute('SELECT id FROM iso WHERE name = ?', (name, )).fetchone() is not None: flash( f"An ISO by the name {name} currently exists in the database. If you wish to overwrite, please remove it first" ) return redirect(url_for('iso.menu')) print(f"setting up {name}") download_image.delay(name) flash(f"Beginning download of {name}, please check back later for status") return redirect(url_for('iso.menu'))
def modify(): username = request.form.get('username') password = request.form.get('password') verify_password = request.form.get('VerifyPassword') if password != verify_password: flash(f"Passwords did not match, no update made") return redirect(url_for('password.menu')) hashed_password = crypt(password, mksalt(METHOD_SHA512)) db = get_db() entry = db.execute('SELECT * FROM passwords WHERE username = ?', (username,)).fetchone() if entry: db.execute('UPDATE passwords SET password_hash = ? WHERE username = ?', (hashed_password, username)) db.commit() flash(f"Password for {username} updated") return redirect(url_for('password.menu')) db.execute('INSERT INTO passwords (username, password_hash) VALUES (?, ?)', (username, hashed_password)) db.commit() flash(f"Password for {username} added") return redirect(url_for('password.menu'))
def add(): if request.method == 'POST': name = request.form.get('name') url = request.form.get('url') username = request.form.get('username') password = request.form.get('password') if name is None or url is None or username is None or password is None: flash("Required information is missing") return redirect(request.url) try: resp = requests.post( f"{url}/api/v1/login", headers={'Content-Type':'application/json'}, data=json.dumps({'username':username, 'password':password}), verify=False ) except requests.exceptions.Timeout: flash("Timeout connecting to conductor") return redirect(request.url) if not resp.ok: flash(f"Conductor returned an error: {resp.status_code} {resp.json()}") return redirect(request.url) token = resp.json()['token'] db = get_db() print(f"name: {name}") print(f"url: {url}") print(f"token: {token}") db.execute('INSERT INTO conductor (name, url, auth_key) VALUES (?, ?, ?)', (name, url, token)) db.commit() flash(f"Conductor {name} added successfully") return redirect(url_for('conductor.menu')) return render_template('conductor_add.html')
def menu(): db = get_db() passwords = db.execute('SELECT username, password_hash FROM passwords').fetchall() return render_template('passwords_manage.html', passwords=passwords)
def update_db_failed(name): db = get_db() db.execute('UPDATE iso SET status = ? WHERE name = ?', ('Failed', name)) db.commit()
def list(): db = get_db() conductors = db.execute('SELECT name FROM conductor').fetchall() return render_template('conductor_list.html', conductors=conductors)
def stage_image(name): nfs_dir = pathlib.Path(constants.IMAGE_FOLDER) / name # Make sure destination doesn't exist try: os.rmdir(nfs_dir) except FileNotFoundError: pass try: os.system(f"osirrox -indev {iso_file(name)} -extract / {nfs_dir}") # If it's already in here, don't re-add it if os.system(f"grep {name} /etc/exports") > 0: fsid = len(open('/etc/exports').readlines()) with open('/etc/exports', 'a') as fh: fh.write( f"{nfs_dir} 192.168.128.0/24(fsid={fsid},no_root_squash)\n" ) os.system('exportfs -ra') except OSError: print( f"There was an error when attempting to setup the NFS share for {name}" ) update_db_failed(name) return False combined_iso = False if (nfs_dir / LEGACY_OTP_KICKSTART_FILE).exists(): ks_file = LEGACY_OTP_KICKSTART_FILE print( f"{name} is a legacy format OTP ISO based on the kickstart file found" ) elif (nfs_dir / LEGACY_STANDARD_KICKSTART_FILE).exists(): ks_file = LEGACY_STANDARD_KICKSTART_FILE print( f"{name} is a legacy format standard ISO based on the kickstart file found" ) elif (nfs_dir / COMBINED_ISO_OTP_KS_FILE).exists() and \ (nfs_dir / COMBINED_ISO_OTP_UEFI_KS_FILE).exists() and \ (nfs_dir / COMBINED_ISO_INTERACTIVE_KS_FILE).exists() and \ (nfs_dir / COMBINED_ISO_INTERACTIVE_UEFI_KS_FILE).exists(): print(f"{name} is a combined ISO based on the kickstart files found") combined_iso = True else: print( f"Could not find either expected kickstart file in {name}, aborting" ) update_db_failed(name) return False uefi_image_dir = pathlib.Path( constants.UEFI_TFTPBOOT_DIR) / "images" / name uefi_image_dir.mkdir(parents=True, exist_ok=True) bios_image_dir = pathlib.Path( constants.BIOS_TFTPBOOT_DIR) / "images" / name bios_image_dir.mkdir(parents=True, exist_ok=True) try: shutil.copy(nfs_dir / "images" / "pxeboot" / "vmlinuz", uefi_image_dir) shutil.copy(nfs_dir / "images" / "pxeboot" / "initrd.img", uefi_image_dir) shutil.copy(nfs_dir / "images" / "pxeboot" / "vmlinuz", bios_image_dir) shutil.copy(nfs_dir / "images" / "pxeboot" / "initrd.img", bios_image_dir) except (FileNotFoundError, OSError): print(f"There was an error setting up TFTP images for {name}") update_db_failed(name) return False uefi_pxelinux_dir = pathlib.Path( constants.UEFI_TFTPBOOT_DIR) / "pxelinux.cfg" uefi_pxelinux_dir.mkdir(parents=True, exist_ok=True) print(f"Writing UEFI pxelinux config for {name}") if combined_iso: with open(uefi_pxelinux_dir / name, "w+") as fh: fh.writelines([ "UI menu.c32\n", "timeout 30\n", "\n", "display boot.msg\n", "\n", "MENU TITLE PXE Boot MENU\n", "\n", f"label {name}\n", f" kernel images/{name}/vmlinuz\n", f" append initrd=http://{constants.UEFI_IP}/images/{name}/initrd.img " f"inst.stage2=nfs:{constants.NFS_IP}:{ pathlib.Path(constants.IMAGE_FOLDER) / name } " f"inst.ks=nfs:{constants.NFS_IP}:{ pathlib.Path(constants.IMAGE_FOLDER) / name }/{ COMBINED_ISO_OTP_UEFI_KS_FILE } " "console=ttyS0,115200n81\n", ]) else: with open(uefi_pxelinux_dir / name, "w+") as fh: fh.writelines([ "UI menu.c32\n", "timeout 30\n", "\n", "display boot.msg\n", "\n", "MENU TITLE PXE Boot MENU\n", "\n", f"label {name}\n", f" kernel images/{name}/vmlinuz\n", f" append initrd=http://{constants.UEFI_IP}/images/{name}/initrd.img " f"inst.stage2=nfs:{constants.NFS_IP}:{ pathlib.Path(constants.IMAGE_FOLDER) / name } " f"inst.ks=nfs:{constants.NFS_IP}:{ pathlib.Path(constants.IMAGE_FOLDER) / name }/{ ks_file } " "console=ttyS0,115200n81\n", ]) bios_pxelinux_dir = pathlib.Path( constants.BIOS_TFTPBOOT_DIR) / "pxelinux.cfg" bios_pxelinux_dir.mkdir(parents=True, exist_ok=True) print(f"Writing BIOS pxelinux config for {name}") if combined_iso: with open(bios_pxelinux_dir / name, "w+") as fh: fh.writelines([ f"default {name}\n", "timeout 30\n", "\n", "display boot.msg\n", "\n", "MENU TITLE PXE Boot MENU\n", "\n", f"label {name}\n", f" kernel images/{name}/vmlinuz\n", f" append initrd=images/{name}/initrd.img " f"inst.stage2=nfs:{constants.NFS_IP}:{ pathlib.Path(constants.IMAGE_FOLDER) / name } " f"inst.ks=nfs:{constants.NFS_IP}:{ pathlib.Path(constants.IMAGE_FOLDER) / name }/{ COMBINED_ISO_OTP_KS_FILE } " "console=ttyS0,115200n81\n", ]) else: with open(bios_pxelinux_dir / name, "w+") as fh: fh.writelines([ f"default {name}\n", "timeout 30\n", "\n", "display boot.msg\n", "\n", "MENU TITLE PXE Boot MENU\n", "\n", f"label {name}\n", f" kernel images/{name}/vmlinuz\n", f" append initrd=images/{name}/initrd.img " f"inst.stage2=nfs:{constants.NFS_IP}:{ pathlib.Path(constants.IMAGE_FOLDER) / name } " f"inst.ks=nfs:{constants.NFS_IP}:{ pathlib.Path(constants.IMAGE_FOLDER) / name }/{ ks_file } " "console=ttyS0,115200n81\n", ]) print( f"Appending new post section to kickstart to post identifier after blast" ) if combined_iso: with open(nfs_dir / COMBINED_ISO_OTP_UEFI_KS_FILE, 'a') as fh: fh.writelines([ '%post\n', 'curl -XPOST http://192.168.128.128/node/add/`dmidecode --string system-serial-number`\n', '%end\n' ]) with open(nfs_dir / COMBINED_ISO_OTP_KS_FILE, 'a') as fh: fh.writelines([ '%post\n', 'curl -XPOST http://192.168.128.128/node/add/`dmidecode --string system-serial-number`\n', '%end\n' ]) else: with open(nfs_dir / ks_file, 'a') as fh: fh.writelines([ '%post\n', 'curl -XPOST http://192.168.128.128/node/add/`dmidecode --string system-serial-number`\n', '%end\n' ]) print(f"Updating password hashes for image {name}") update_password(name) print(f"Image {name} appears to have been setup correctly, updating DB") db = get_db() db.execute('UPDATE iso SET status = ? WHERE name = ?', ('Ready', name)) db.commit() return True
def associate_quickstart_to_node(qs_id, identifier): db = get_db() db.execute('UPDATE node SET quickstart_id = ? WHERE identifier = ?', (qs_id, identifier)) db.commit()
def disassociate_quickstart_from_node(identifier): db = get_db() db.execute('UPDATE node SET quickstart_id = null WHERE identifier = ?', (identifier, )) db.commit()
def list(): db = get_db() quickstarts = db.execute('SELECT * FROM quickstart').fetchall() return render_template('quickstart_list.html', quickstarts=quickstarts)
def clear_default_qs(): db = get_db() # Get rid of any existing defaults db.execute('UPDATE quickstart SET default_quickstart = 0 WHERE default_quickstart > 0') db.commit()
def list(): db = get_db() isos = db.execute('SELECT name, status FROM iso').fetchall() active = get_active() return render_template('iso_list.html', isos=isos, active=active)