Initial work on bootstrap setup
This commit is contained in:
parent
cbc70e2ef8
commit
fc890cc606
|
@ -0,0 +1,68 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# bootstrap_http_server.py - PVC bootstrap HTTP server class
|
||||||
|
# Part of the Parallel Virtual Cluster (PVC) system
|
||||||
|
#
|
||||||
|
# Copyright (C) 2018 Joshua M. Boniface <joshua@boniface.me>
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
import http.server
|
||||||
|
import socketserver
|
||||||
|
import atexit
|
||||||
|
import json
|
||||||
|
|
||||||
|
LISTEN_PORT = 10080
|
||||||
|
|
||||||
|
next_nodeid = 0
|
||||||
|
|
||||||
|
def get_next_nodeid():
|
||||||
|
global next_nodeid
|
||||||
|
new_nodeid = next_nodeid + 1
|
||||||
|
next_nodeid = new_nodeid
|
||||||
|
return str(new_nodeid)
|
||||||
|
|
||||||
|
class CheckinHandler(http.server.BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path == '/node_checkin':
|
||||||
|
# A node is checking in
|
||||||
|
print('Node checking in...')
|
||||||
|
# Get the next available ID
|
||||||
|
node_id = get_next_nodeid()
|
||||||
|
print('Assigned new node ID {}'.format(node_id))
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-type", "application/json")
|
||||||
|
self.end_headers()
|
||||||
|
pvcd_conf = 'test'
|
||||||
|
install_script = """#!/bin/bash
|
||||||
|
echo "Hello, world!"
|
||||||
|
"""
|
||||||
|
body = { 'node_id': node_id, 'pvcd_conf': pvcd_conf, 'install_script': install_script }
|
||||||
|
self.wfile.write(json.dumps(body).encode('ascii'))
|
||||||
|
else:
|
||||||
|
self.send_error(404)
|
||||||
|
|
||||||
|
httpd = socketserver.TCPServer(("", LISTEN_PORT), CheckinHandler, bind_and_activate=False)
|
||||||
|
httpd.allow_reuse_address = True
|
||||||
|
httpd.server_bind()
|
||||||
|
httpd.server_activate()
|
||||||
|
|
||||||
|
def cleanup():
|
||||||
|
httpd.shutdown()
|
||||||
|
atexit.register(cleanup)
|
||||||
|
|
||||||
|
print("serving at port", LISTEN_PORT)
|
||||||
|
httpd.serve_forever()
|
|
@ -34,6 +34,7 @@ import client_lib.node as pvc_node
|
||||||
import client_lib.vm as pvc_vm
|
import client_lib.vm as pvc_vm
|
||||||
import client_lib.network as pvc_network
|
import client_lib.network as pvc_network
|
||||||
import client_lib.ceph as pvc_ceph
|
import client_lib.ceph as pvc_ceph
|
||||||
|
import client_lib.provisioner as pvc_provisioner
|
||||||
|
|
||||||
myhostname = socket.gethostname()
|
myhostname = socket.gethostname()
|
||||||
zk_host = ''
|
zk_host = ''
|
||||||
|
@ -188,6 +189,56 @@ def cli_vm():
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# pvc vm add
|
||||||
|
###############################################################################
|
||||||
|
@click.command(name='add', short_help='Add a new virtual machine to the provisioning queue.')
|
||||||
|
@click.option(
|
||||||
|
'--node', 'target_node',
|
||||||
|
help='Home node for this domain; autodetect if unspecified.'
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--cluster', 'is_cluster',
|
||||||
|
is_flag=True,
|
||||||
|
help='Create a cluster VM.'
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--system-template', 'system_template',
|
||||||
|
required=True,
|
||||||
|
help='System resource template for this domain.'
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--network-template', 'network_template',
|
||||||
|
required=True,
|
||||||
|
help='Network resource template for this domain.'
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--storage-template', 'storage_template',
|
||||||
|
required=True,
|
||||||
|
help='Storage resource template for this domain.'
|
||||||
|
)
|
||||||
|
@click.argument(
|
||||||
|
'vmname'
|
||||||
|
)
|
||||||
|
def vm_add(vmname, target_node, is_cluster, system_template, network_template, storage_template):
|
||||||
|
"""
|
||||||
|
Add a new VM VMNAME to the provisioning queue.
|
||||||
|
|
||||||
|
Note: Cluster VMs are those which will only run on Coordinator hosts. Usually, these VMs will use the 'cluster' network template, or possibly a custom template including the upstream network as well. Use these sparingly, as they are designed primarily for cluster control or upstream bridge VMs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
zk_conn = pvc_common.startZKConnection(zk_host)
|
||||||
|
retcode, retmsg = pvc_provisioner.add_vm(
|
||||||
|
zk_conn,
|
||||||
|
vmname=vmname,
|
||||||
|
target_node=target_node,
|
||||||
|
is_cluster=is_cluster,
|
||||||
|
system_template=system_template,
|
||||||
|
network_template=network_template,
|
||||||
|
storage_template=storage_template
|
||||||
|
)
|
||||||
|
cleanup(retcode, retmsg, zk_conn)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# pvc vm define
|
# pvc vm define
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
@ -1163,47 +1214,15 @@ def ceph_pool_list(limit):
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# pvc init
|
# pvc init
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
@click.command(name='init', short_help='Initialize a new cluster.')
|
@click.command(name='init', short_help='Initialize a new cluster.')
|
||||||
@click.option('--yes', is_flag=True,
|
|
||||||
expose_value=False,
|
|
||||||
prompt='DANGER: This command will destroy any existing cluster data. Do you want to continue?')
|
|
||||||
def init_cluster():
|
def init_cluster():
|
||||||
"""
|
"""
|
||||||
Perform initialization of Zookeeper to act as a PVC cluster.
|
Perform initialization of a new PVC cluster.
|
||||||
|
|
||||||
DANGER: This command will overwrite any existing cluster data and provision a new cluster at the specified Zookeeper connection string. Do not run this against a cluster unless you are sure this is what you want.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
click.echo('Initializing a new cluster with Zookeeper address "{}".'.format(zk_host))
|
import pvc_init
|
||||||
|
pvc_init.run()
|
||||||
# Open a Zookeeper connection
|
|
||||||
zk_conn = pvc_common.startZKConnection(zk_host)
|
|
||||||
|
|
||||||
# Destroy the existing data
|
|
||||||
try:
|
|
||||||
zk_conn.delete('/networks', recursive=True)
|
|
||||||
zk_conn.delete('/domains', recursive=True)
|
|
||||||
zk_conn.delete('/nodes', recursive=True)
|
|
||||||
zk_conn.delete('/primary_node', recursive=True)
|
|
||||||
zk_conn.delete('/ceph', recursive=True)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Create the root keys
|
|
||||||
transaction = zk_conn.transaction()
|
|
||||||
transaction.create('/nodes', ''.encode('ascii'))
|
|
||||||
transaction.create('/primary_node', 'none'.encode('ascii'))
|
|
||||||
transaction.create('/domains', ''.encode('ascii'))
|
|
||||||
transaction.create('/networks', ''.encode('ascii'))
|
|
||||||
transaction.create('/ceph', ''.encode('ascii'))
|
|
||||||
transaction.create('/ceph/osds', ''.encode('ascii'))
|
|
||||||
transaction.create('/ceph/pools', ''.encode('ascii'))
|
|
||||||
transaction.commit()
|
|
||||||
|
|
||||||
# Close the Zookeeper connection
|
|
||||||
pvc_common.stopZKConnection(zk_conn)
|
|
||||||
|
|
||||||
click.echo('Successfully initialized new cluster. Any running PVC daemons will need to be restarted.')
|
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
@ -1256,6 +1275,7 @@ cli_node.add_command(node_unflush)
|
||||||
cli_node.add_command(node_info)
|
cli_node.add_command(node_info)
|
||||||
cli_node.add_command(node_list)
|
cli_node.add_command(node_list)
|
||||||
|
|
||||||
|
cli_vm.add_command(vm_add)
|
||||||
cli_vm.add_command(vm_define)
|
cli_vm.add_command(vm_define)
|
||||||
cli_vm.add_command(vm_modify)
|
cli_vm.add_command(vm_modify)
|
||||||
cli_vm.add_command(vm_undefine)
|
cli_vm.add_command(vm_undefine)
|
||||||
|
|
|
@ -0,0 +1,658 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# pvcd.py - PVC client command-line interface
|
||||||
|
# Part of the Parallel Virtual Cluster (PVC) system
|
||||||
|
#
|
||||||
|
# Copyright (C) 2018 Joshua M. Boniface <joshua@boniface.me>
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
import locale
|
||||||
|
import socket
|
||||||
|
import click
|
||||||
|
import tempfile
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
import difflib
|
||||||
|
import re
|
||||||
|
import yaml
|
||||||
|
import colorama
|
||||||
|
import netifaces
|
||||||
|
import ipaddress
|
||||||
|
import urllib.request
|
||||||
|
import tarfile
|
||||||
|
|
||||||
|
from dialog import Dialog
|
||||||
|
|
||||||
|
import client_lib.common as pvc_common
|
||||||
|
import client_lib.node as pvc_node
|
||||||
|
|
||||||
|
|
||||||
|
# Repository configurations
|
||||||
|
#deb_mirror = "ftp.debian.org"
|
||||||
|
deb_mirror = "deb1.i.bonilan.net:3142"
|
||||||
|
deb_release = "buster"
|
||||||
|
deb_arch = "amd64"
|
||||||
|
deb_packages = "mdadm,lvm2,parted,gdisk,debootstrap,grub-pc,linux-image-amd64"
|
||||||
|
|
||||||
|
# Scripts
|
||||||
|
cluster_floating_ip = "10.10.1.254"
|
||||||
|
bootstrap_script = """#!/bin/bash
|
||||||
|
# Check in and get our nodename, pvcd.conf, and install script
|
||||||
|
output="$( curl {}:10080/node_checkin )"
|
||||||
|
# Export node_id
|
||||||
|
node_id="$( jq -r '.node_id' <<<"${output}" )"
|
||||||
|
export node_id
|
||||||
|
# Export pvcd.conf
|
||||||
|
pvcd_conf="$( jq -r '.pvcd_conf' <<<"${output}" )"
|
||||||
|
export pvcd_conf
|
||||||
|
# Execute install script
|
||||||
|
jq -r '.install_script' <<<"${output}" | bash
|
||||||
|
"""
|
||||||
|
install_script = """#!/bin/bash
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Run a oneshot command, optionally without blocking
|
||||||
|
def run_os_command(command_string, environment=None):
|
||||||
|
command = command_string.split()
|
||||||
|
try:
|
||||||
|
command_output = subprocess.run(
|
||||||
|
command,
|
||||||
|
env=environment,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return 1, "", ""
|
||||||
|
|
||||||
|
retcode = command_output.returncode
|
||||||
|
try:
|
||||||
|
stdout = command_output.stdout.decode('ascii')
|
||||||
|
except:
|
||||||
|
stdout = ''
|
||||||
|
try:
|
||||||
|
stderr = command_output.stderr.decode('ascii')
|
||||||
|
except:
|
||||||
|
stderr = ''
|
||||||
|
return retcode, stdout, stderr
|
||||||
|
|
||||||
|
|
||||||
|
# Start the initalization of a new cluster
|
||||||
|
def orun():
|
||||||
|
locale.setlocale(locale.LC_ALL, '')
|
||||||
|
|
||||||
|
# You may want to use 'autowidgetsize=True' here (requires pythondialog >= 3.1)
|
||||||
|
d = Dialog(dialog="dialog", autowidgetsize=True)
|
||||||
|
de = Dialog(dialog="dialog", autowidgetsize=True)
|
||||||
|
# Dialog.set_background_title() requires pythondialog 2.13 or later
|
||||||
|
d.set_background_title("PVC Cluster Initialization")
|
||||||
|
|
||||||
|
# Initial message
|
||||||
|
d.msgbox("""Welcome to the PVC cluster initalization tool. This tool
|
||||||
|
will ask you several questions about the cluster, and then
|
||||||
|
perform the required tasks to bootstrap the cluster.
|
||||||
|
|
||||||
|
PLEASE READ ALL SCREENS CAREFULLY.
|
||||||
|
|
||||||
|
Before proceeding, ensure that:
|
||||||
|
(a) This system is connected, wired if possible, to a switch.
|
||||||
|
(b) This system has a second network connection with Internet
|
||||||
|
connectivity and is able to download files.
|
||||||
|
(c) The initial nodes are powered off, connected to the
|
||||||
|
mentioned switch, and are configured to boot from PXE.
|
||||||
|
(d) All non-system disks are disconnected from all nodes.
|
||||||
|
Storage disks will be added after bootstrapping.
|
||||||
|
|
||||||
|
Once these prerequisites are complete, press Enter to proceed.
|
||||||
|
""")
|
||||||
|
|
||||||
|
#
|
||||||
|
# Phase 0 - get our local interface
|
||||||
|
#
|
||||||
|
interfaces = netifaces.interfaces()
|
||||||
|
interface_list = list()
|
||||||
|
for idx, val in enumerate(interfaces):
|
||||||
|
interface_list.append(("{}".format(idx), "{}".format(val)))
|
||||||
|
code, index = d.menu("""Select a network interface to use for cluster bootstrapping.""",
|
||||||
|
choices=interface_list)
|
||||||
|
if code == d.CANCEL:
|
||||||
|
print("Aborted.")
|
||||||
|
exit(0)
|
||||||
|
interface = interfaces[int(index)]
|
||||||
|
|
||||||
|
#
|
||||||
|
# Phase 1 - coordinator list
|
||||||
|
#
|
||||||
|
code, coordinator_count = d.menu("Select the number of initial (coordinator) nodes:",
|
||||||
|
choices=[("1", "Testing and very small non-redundant deployments"),
|
||||||
|
("3", "Standard (3-20) hypervisor deployments"),
|
||||||
|
("5", "Large (21-99) hypervisor deployments")])
|
||||||
|
coordinator_count = int(coordinator_count)
|
||||||
|
if code == d.CANCEL:
|
||||||
|
print("Aborted.")
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Phase 2 - Get the networks
|
||||||
|
#
|
||||||
|
d.msgbox("""The next screens will ask for the cluster networks in CIDR
|
||||||
|
format as well as a floating IP in each. The networks are:
|
||||||
|
(a) Cluster: Used by the nodes to communicate VXLANs and
|
||||||
|
pass virtualization (migration) traffic between each
|
||||||
|
other. Each node will be assigned an address in this
|
||||||
|
network equal to its node ID (e.g. node1 at .1, etc.).
|
||||||
|
Each node with IPMI support will be assigned an IPMI
|
||||||
|
address in this network equal to its node ID plus 120
|
||||||
|
(e.g. node1-lom at .121, etc.). IPs 241-254 will be
|
||||||
|
reserved for cluster management; the floating IP should
|
||||||
|
be in this range.
|
||||||
|
(b) Storage: Used by the nodes to pass storage traffic
|
||||||
|
between each other, both for Ceph OSDs and for RBD
|
||||||
|
access. Each node will be assigned an address in this
|
||||||
|
network equal to its node ID. IPs 241-254 will be
|
||||||
|
reserved for cluster management; the floating IP should
|
||||||
|
be in this range.
|
||||||
|
(c) Upstream: Used by the nodes to communicate upstream
|
||||||
|
outside of the cluster. This network has several
|
||||||
|
functions depending on the configuration of the
|
||||||
|
virtual networks; relevant questions will be asked
|
||||||
|
later in the configuration.
|
||||||
|
* The first two networks are dedicated to the cluster. They
|
||||||
|
should be RFC1918 private networks and be sized sufficiently
|
||||||
|
for the future growth of the cluster; a /24 is recommended
|
||||||
|
for most situations and will support up to 99 nodes.
|
||||||
|
* The third network, as mentioned, has several potential
|
||||||
|
functions depending on the final network configuration of the
|
||||||
|
cluster. It should already exist, and nodes may or may not
|
||||||
|
have individual addresses in this network. Further questions
|
||||||
|
about this network will be asked later during setup.
|
||||||
|
* All networks should have a DNS domain which will be asked
|
||||||
|
during this stage. For the first two networks, the domain
|
||||||
|
may be private and unresolvable outside the network if
|
||||||
|
desired; the third should be a valid but will generally
|
||||||
|
be unused in the administration of the cluster. The FQDNs
|
||||||
|
of each node will contain the Cluster domain.
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Get the primary cluster network
|
||||||
|
valid_network = False
|
||||||
|
message = "Enter the new cluster's primary network in CIDR format."
|
||||||
|
while not valid_network:
|
||||||
|
code, network = d.inputbox(message)
|
||||||
|
if code == d.CANCEL:
|
||||||
|
print("Aborted.")
|
||||||
|
exit(0)
|
||||||
|
try:
|
||||||
|
cluster_network = ipaddress.ip_network(network)
|
||||||
|
valid_network = True
|
||||||
|
except ValueError:
|
||||||
|
message = "Error - network {} is not valid.\n\nEnter the new cluster's primary network in CIDR format.".format(network)
|
||||||
|
continue
|
||||||
|
|
||||||
|
valid_address = False
|
||||||
|
message = "Enter the CIDR floating IP address for the cluster's primary network."
|
||||||
|
while not valid_address:
|
||||||
|
code, address = d.inputbox(message)
|
||||||
|
if code == d.CANCEL:
|
||||||
|
print("Aborted.")
|
||||||
|
exit (0)
|
||||||
|
try:
|
||||||
|
cluster_floating_ip = ipaddress.ip_address(address)
|
||||||
|
if not cluster_floating_ip in list(cluster_network.hosts()):
|
||||||
|
message = "Error - address {} is not in network {}.\n\nEnter the CIDR floating IP address for the cluster's primary network.".format(cluster_floating_ip, cluster_network)
|
||||||
|
continue
|
||||||
|
valid_address = True
|
||||||
|
except ValueError:
|
||||||
|
message = "Error - address {} is not valid.\n\nEnter the CIDR floating IP address for the cluster's primary network.".format(cluster_floating_ip, cluster_network)
|
||||||
|
continue
|
||||||
|
|
||||||
|
code, cluster_domain = d.inputbox("""Enter the new cluster's primary DNS domain.""")
|
||||||
|
if code == d.CANCEL:
|
||||||
|
print("Aborted.")
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
# Get the storage network
|
||||||
|
valid_network = False
|
||||||
|
message = "Enter the new cluster's storage network in CIDR format."
|
||||||
|
while not valid_network:
|
||||||
|
code, network = d.inputbox(message)
|
||||||
|
if code == d.CANCEL:
|
||||||
|
print("Aborted.")
|
||||||
|
exit(0)
|
||||||
|
try:
|
||||||
|
storage_network = ipaddress.ip_network(network)
|
||||||
|
valid_network = True
|
||||||
|
except ValueError:
|
||||||
|
message = "Error - network {} is not valid.\n\nEnter the new cluster's storage network in CIDR format.".format(network)
|
||||||
|
continue
|
||||||
|
|
||||||
|
valid_address = False
|
||||||
|
message = "Enter the CIDR floating IP address for the cluster's storage network."
|
||||||
|
while not valid_address:
|
||||||
|
code, address = d.inputbox(message)
|
||||||
|
if code == d.CANCEL:
|
||||||
|
print("Aborted.")
|
||||||
|
exit (0)
|
||||||
|
try:
|
||||||
|
storage_floating_ip = ipaddress.ip_address(address)
|
||||||
|
if not storage_floating_ip in list(storage_network.hosts()):
|
||||||
|
message = "Error - address {} is not in network {}.\n\nEnter the CIDR floating IP address for the cluster's storage network.".format(storage_floating_ip, storage_network)
|
||||||
|
continue
|
||||||
|
valid_address = True
|
||||||
|
except ValueError:
|
||||||
|
message = "Error - address {} is not valid.\n\nEnter the CIDR floating IP address for the cluster's storage network.".format(storage_floating_ip, storage_network)
|
||||||
|
continue
|
||||||
|
|
||||||
|
code, storage_domain = d.inputbox("""Enter the new cluster's storage DNS domain.""")
|
||||||
|
if code == d.CANCEL:
|
||||||
|
print("Aborted.")
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
# Get the upstream network
|
||||||
|
valid_network = False
|
||||||
|
message = "Enter the new cluster's upstream network in CIDR format."
|
||||||
|
while not valid_network:
|
||||||
|
code, network = d.inputbox(message)
|
||||||
|
if code == d.CANCEL:
|
||||||
|
print("Aborted.")
|
||||||
|
exit(0)
|
||||||
|
try:
|
||||||
|
upstream_network = ipaddress.ip_network(network)
|
||||||
|
valid_network = True
|
||||||
|
except ValueError:
|
||||||
|
message = "Error - network {} is not valid.\n\nEnter the new cluster's upstream network in CIDR format.".format(network)
|
||||||
|
continue
|
||||||
|
|
||||||
|
valid_address = False
|
||||||
|
message = "Enter the CIDR floating IP address for the cluster's upstream network."
|
||||||
|
while not valid_address:
|
||||||
|
code, address = d.inputbox(message)
|
||||||
|
if code == d.CANCEL:
|
||||||
|
print("Aborted.")
|
||||||
|
exit (0)
|
||||||
|
try:
|
||||||
|
upstream_floating_ip = ipaddress.ip_address(address)
|
||||||
|
if not upstream_floating_ip in list(upstream_network.hosts()):
|
||||||
|
message = "Error - address {} is not in network {}.\n\nEnter the CIDR floating IP address for the cluster's upstream network.".format(upstream_floating_ip, upstream_network)
|
||||||
|
continue
|
||||||
|
valid_address = True
|
||||||
|
except ValueError:
|
||||||
|
message = "Error - address {} is not valid.\n\nEnter the CIDR floating IP address for the cluster's upstream network.".format(upstream_floating_ip, upstream_network)
|
||||||
|
continue
|
||||||
|
|
||||||
|
code, upstream_domain = d.inputbox("""Enter the new cluster's upstream DNS domain.""")
|
||||||
|
if code == d.CANCEL:
|
||||||
|
print("Aborted.")
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Phase 3 - Upstream settings
|
||||||
|
#
|
||||||
|
d.msgbox("""The next screens will present several questions regarding
|
||||||
|
the upstream and guest network configuration for the new
|
||||||
|
cluster, in an attempt to determine some default values
|
||||||
|
for the initial template files. Most of these options can
|
||||||
|
be overridden later by the client configuration tool or by
|
||||||
|
manual modification of the node configuration files, but
|
||||||
|
will shape the initial VM configuration and node config
|
||||||
|
file.
|
||||||
|
""")
|
||||||
|
|
||||||
|
if d.yesno("""Should the PVC cluster manage client IP addressing?""") == d.OK:
|
||||||
|
enable_routing = True
|
||||||
|
else:
|
||||||
|
enable_routing = False
|
||||||
|
|
||||||
|
if d.yesno("""Should the PVC cluster provide NAT functionality?""") == d.OK:
|
||||||
|
enable_nat = True
|
||||||
|
else:
|
||||||
|
enable_nat = False
|
||||||
|
|
||||||
|
if d.yesno("""Should the PVC cluster manage client DNS records?""") == d.OK:
|
||||||
|
enable_dns = True
|
||||||
|
else:
|
||||||
|
enable_dns = False
|
||||||
|
|
||||||
|
#
|
||||||
|
# Phase 4 - Configure templates
|
||||||
|
#
|
||||||
|
d.msgbox("""The next screens will present templates for several
|
||||||
|
configuration files in your $EDITOR, based on the options
|
||||||
|
selected above. These templates will be distributed to the
|
||||||
|
cluster nodes during bootstrapping.
|
||||||
|
|
||||||
|
Various values are indicated for '<replacement>' by you,
|
||||||
|
as 'TEMPLATE' values to be filled in from other information,
|
||||||
|
gained during these dialogs, or as default values.
|
||||||
|
|
||||||
|
Once you are finished editing the files, write and quit the
|
||||||
|
editor.
|
||||||
|
|
||||||
|
For more information on any particular field, see the PVC
|
||||||
|
documentation.
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Generate the node interfaces file template
|
||||||
|
interfaces_configuration = """#
|
||||||
|
# pvc node network interfaces file
|
||||||
|
#
|
||||||
|
# Writing this template requires knowledge of the default
|
||||||
|
# persistent network names of the target server class.
|
||||||
|
#
|
||||||
|
# Configure any required bonding here, however do not
|
||||||
|
# configure any vLANs or VXLANs as those are managed
|
||||||
|
# by the PVC daemon itself.
|
||||||
|
#
|
||||||
|
# Make note of the interfaces specified for each type,
|
||||||
|
# as these will be required in the daemon config as
|
||||||
|
# well.
|
||||||
|
#
|
||||||
|
# Note that the Cluster and Storage networks *may* use
|
||||||
|
# the same underlying network device; in which case,
|
||||||
|
# only define one here and specify the same device
|
||||||
|
# for both networks in the daemon config.
|
||||||
|
|
||||||
|
auto lo
|
||||||
|
iface lo inet loopback
|
||||||
|
|
||||||
|
# Upstream physical interface
|
||||||
|
auto <upstream_dev_interface>
|
||||||
|
iface <upstream_dev_interface> inet manual
|
||||||
|
|
||||||
|
# Cluster physical interface
|
||||||
|
auto <cluster_dev_interface>
|
||||||
|
iface <cluster_dev_interface> inet manual
|
||||||
|
|
||||||
|
# Storage physical interface
|
||||||
|
auto <storage_dev_interface>
|
||||||
|
iface <storage_dev_interface> inet manual
|
||||||
|
"""
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".tmp") as tf:
|
||||||
|
EDITOR = os.environ.get('EDITOR', 'vi')
|
||||||
|
tf.write(interfaces_configuration.encode("utf-8"))
|
||||||
|
tf.flush()
|
||||||
|
subprocess.call([EDITOR, tf.name])
|
||||||
|
tf.seek(0)
|
||||||
|
interfaces_configuration = tf.read().decode("utf-8")
|
||||||
|
|
||||||
|
# Generate the configuration file template
|
||||||
|
coordinator_list = list()
|
||||||
|
for i in range(0,coordinator_count):
|
||||||
|
coordinator_list.append("node{}".format(i + 1))
|
||||||
|
dnssql_password = "Sup3rS3cr37SQL"
|
||||||
|
ipmi_password = "Sup3rS3cr37IPMI"
|
||||||
|
pvcd_configuration = {
|
||||||
|
"pvc": {
|
||||||
|
"node": "NODENAME",
|
||||||
|
"cluster": {
|
||||||
|
"coordinators": coordinator_list,
|
||||||
|
"networks": {
|
||||||
|
"upstream": {
|
||||||
|
"domain": upstream_domain,
|
||||||
|
"network": str(upstream_network),
|
||||||
|
"floating_ip": str(upstream_floating_ip)
|
||||||
|
},
|
||||||
|
"cluster": {
|
||||||
|
"domain": cluster_domain,
|
||||||
|
"network": str(cluster_network),
|
||||||
|
"floating_ip": str(cluster_floating_ip)
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"domain": storage_domain,
|
||||||
|
"network": str(storage_network),
|
||||||
|
"floating_ip": str(storage_floating_ip)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"coordinator": {
|
||||||
|
"dns": {
|
||||||
|
"database": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": "3306",
|
||||||
|
"name": "pvcdns",
|
||||||
|
"user": "pvcdns",
|
||||||
|
"pass": dnssql_password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"system": {
|
||||||
|
"fencing": {
|
||||||
|
"intervals": {
|
||||||
|
"keepalive_interval": "5",
|
||||||
|
"fence_intervals": "6",
|
||||||
|
"suicide_intervals": "0"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"successful_fence": "migrate",
|
||||||
|
"failed_fence": "None"
|
||||||
|
},
|
||||||
|
"ipmi": {
|
||||||
|
"address": "by-id",
|
||||||
|
"user": "pvcipmi",
|
||||||
|
"pass": ipmi_password
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"migration": {
|
||||||
|
"target_selector": "mem"
|
||||||
|
},
|
||||||
|
"configuration":{
|
||||||
|
"directories": {
|
||||||
|
"dynamic_directory": "/run/pvc",
|
||||||
|
"log_directory": "/var/log/pvc"
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"file_logging": "True",
|
||||||
|
"stdout_logging": "True"
|
||||||
|
},
|
||||||
|
"networking": {
|
||||||
|
"upstream": {
|
||||||
|
"device": "<upstream_interface_dev>",
|
||||||
|
"address": "None"
|
||||||
|
},
|
||||||
|
"cluster": {
|
||||||
|
"device": "<cluster_interface_dev>",
|
||||||
|
"address": "by-id"
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"device": "<storage_interface_dev>",
|
||||||
|
"address": "by-id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pvcd_configuration_header = """#
|
||||||
|
# pvcd node configuration file
|
||||||
|
#
|
||||||
|
# For full details on the available options, consult the PVC documentation.
|
||||||
|
#
|
||||||
|
# The main pertanent replacements are:
|
||||||
|
# <upstream_interface_dev>: the upstream device name from the interface template
|
||||||
|
# <cluster_interface_dev>: the cluster device name from the interface template
|
||||||
|
# <storage_interface_dev>: the storage device name from the interface template
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".tmp") as tf:
|
||||||
|
EDITOR = os.environ.get('EDITOR', 'vi')
|
||||||
|
pvcd_configuration_string = pvcd_configuration_header + yaml.dump(pvcd_configuration, default_style='"', default_flow_style=False)
|
||||||
|
tf.write(pvcd_configuration_string.encode("utf-8"))
|
||||||
|
tf.flush()
|
||||||
|
subprocess.call([EDITOR, tf.name])
|
||||||
|
tf.seek(0)
|
||||||
|
pvcd_configuration = yaml.load(tf.read().decode("utf-8"))
|
||||||
|
|
||||||
|
# We now have all the details to begin
|
||||||
|
# - interface
|
||||||
|
# - coordinator_count
|
||||||
|
# - cluster_network
|
||||||
|
# - cluster_floating_ip
|
||||||
|
# - cluster_domain
|
||||||
|
# - storage_network
|
||||||
|
# - storage_floating_ip
|
||||||
|
# - storage_domain
|
||||||
|
# - upstream_network
|
||||||
|
# - upstream_floating_ip
|
||||||
|
# - upstream_domain
|
||||||
|
# - enable_routing
|
||||||
|
# - enable_nat
|
||||||
|
# - enable_dns
|
||||||
|
# - interfaces_configuration [template]
|
||||||
|
# - coordinator_list
|
||||||
|
# - dnssql_password
|
||||||
|
# - ipmi_password
|
||||||
|
# - pvcd_configuration [ template]
|
||||||
|
|
||||||
|
d.msgbox("""Information gathering complete. The PVC bootstrap
|
||||||
|
utility will now prepare the local system:
|
||||||
|
(a) Generate the node bootstrap image(s).
|
||||||
|
(b) Start up dnsmasq listening on the interface.
|
||||||
|
""")
|
||||||
|
|
||||||
|
def run():
|
||||||
|
# Begin preparing the local system - install required packages
|
||||||
|
required_packages = [
|
||||||
|
'dnsmasq',
|
||||||
|
'debootstrap',
|
||||||
|
'debconf-utils',
|
||||||
|
'squashfs-tools',
|
||||||
|
'live-boot',
|
||||||
|
'ansible'
|
||||||
|
]
|
||||||
|
apt_command = "sudo apt install -y " + ' '.join(required_packages)
|
||||||
|
retcode, stdout, stderr = run_os_command(apt_command)
|
||||||
|
print(stdout)
|
||||||
|
if retcode:
|
||||||
|
print("ERROR: Package installation failed. Aborting setup.")
|
||||||
|
print(stderr)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Generate a TFTP image for the installer
|
||||||
|
#
|
||||||
|
|
||||||
|
# Create our temporary working directory
|
||||||
|
print("Create temporary directory...")
|
||||||
|
tempdir = tempfile.mkdtemp()
|
||||||
|
print(" > " + tempdir)
|
||||||
|
|
||||||
|
# Download the netboot files
|
||||||
|
print("Download PXE boot files...")
|
||||||
|
download_path = "http://{mirror}/debian/dists/{release}/main/installer-{arch}/current/images/netboot/netboot.tar.gz".format(
|
||||||
|
mirror=deb_mirror,
|
||||||
|
release=deb_release,
|
||||||
|
arch=deb_arch
|
||||||
|
)
|
||||||
|
bootarchive_file, headers = urllib.request.urlretrieve (download_path, tempdir + "/netboot.tar.gz")
|
||||||
|
print(" > " + bootarchive_file)
|
||||||
|
|
||||||
|
# Extract the netboot files
|
||||||
|
print("Extract PXE boot files...")
|
||||||
|
with tarfile.open(bootarchive_file) as tar:
|
||||||
|
tar.extractall(tempdir + "/bootfiles")
|
||||||
|
|
||||||
|
# Prepare a bare system with debootstrap
|
||||||
|
print("Prepare installer debootstrap install...")
|
||||||
|
debootstrap_command = "sudo -u root debootstrap --include={instpkg} {release} {tempdir}/rootfs http://{mirror}/debian".format(
|
||||||
|
instpkg=deb_packages,
|
||||||
|
release=deb_release,
|
||||||
|
tempdir=tempdir,
|
||||||
|
mirror=deb_mirror
|
||||||
|
)
|
||||||
|
retcode, stdout, stderr = run_os_command(debootstrap_command)
|
||||||
|
if retcode:
|
||||||
|
print("ERROR: Debootstrap failed. Aborting setup.")
|
||||||
|
print(stdout)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
# Prepare some useful configuration tweaks
|
||||||
|
print("Tweaking installed image for boot...")
|
||||||
|
sedtty_command = """sudo -u root sed -i
|
||||||
|
's|/sbin/agetty --noclear|/sbin/agetty --noclear --autologin root|g'
|
||||||
|
{}/rootfs/etc/systemd/system/getty@tty1.service""".format(tempdir)
|
||||||
|
retcode, stdout, stderr = run_os_command(sedtty_command)
|
||||||
|
|
||||||
|
# "Fix" permissions so we can write
|
||||||
|
retcode, stdout, stderr = run_os_command("sudo chmod 777 {}/rootfs/root".format(tempdir))
|
||||||
|
retcode, stdout, stderr = run_os_command("sudo chmod 666 {}/rootfs/root/.bashrc".format(tempdir))
|
||||||
|
# Write the install script to root's bashrc
|
||||||
|
with open("{}/rootfs/root/.bashrc".format(tempdir), "w") as bashrcf:
|
||||||
|
bashrcf.write(bootstrap_script)
|
||||||
|
# Restore permissions
|
||||||
|
retcode, stdout, stderr = run_os_command("sudo chmod 600 {}/rootfs/root/.bashrc".format(tempdir))
|
||||||
|
retcode, stdout, stderr = run_os_command("sudo chmod 700 {}/rootfs/root".format(tempdir))
|
||||||
|
|
||||||
|
# Create the squashfs
|
||||||
|
print("Create the squashfs...")
|
||||||
|
squashfs_command = "sudo nice mksquashfs {tempdir}/rootfs {tempdir}/bootfiles/installer.squashfs".format(
|
||||||
|
tempdir=tempdir
|
||||||
|
)
|
||||||
|
retcode, stdout, stderr = run_os_command(squashfs_command)
|
||||||
|
if retcode:
|
||||||
|
print("ERROR: SquashFS creation failed. Aborting setup.")
|
||||||
|
print(stderr)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Prepare the DHCP and TFTP dnsmasq daemon
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Prepare the HTTP listenener for the first node
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Initialize the Zookeeper cluster
|
||||||
|
#
|
||||||
|
def init_zookeeper(zk_host):
|
||||||
|
click.echo('Initializing a new cluster with Zookeeper address "{}".'.format(zk_host))
|
||||||
|
|
||||||
|
# Open a Zookeeper connection
|
||||||
|
zk_conn = pvc_common.startZKConnection(zk_host)
|
||||||
|
|
||||||
|
# Destroy the existing data
|
||||||
|
try:
|
||||||
|
zk_conn.delete('/networks', recursive=True)
|
||||||
|
zk_conn.delete('/domains', recursive=True)
|
||||||
|
zk_conn.delete('/nodes', recursive=True)
|
||||||
|
zk_conn.delete('/primary_node', recursive=True)
|
||||||
|
zk_conn.delete('/ceph', recursive=True)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Create the root keys
|
||||||
|
transaction = zk_conn.transaction()
|
||||||
|
transaction.create('/nodes', ''.encode('ascii'))
|
||||||
|
transaction.create('/primary_node', 'none'.encode('ascii'))
|
||||||
|
transaction.create('/domains', ''.encode('ascii'))
|
||||||
|
transaction.create('/networks', ''.encode('ascii'))
|
||||||
|
transaction.create('/ceph', ''.encode('ascii'))
|
||||||
|
transaction.create('/ceph/osds', ''.encode('ascii'))
|
||||||
|
transaction.create('/ceph/pools', ''.encode('ascii'))
|
||||||
|
transaction.commit()
|
||||||
|
|
||||||
|
# Close the Zookeeper connection
|
||||||
|
pvc_common.stopZKConnection(zk_conn)
|
||||||
|
|
||||||
|
click.echo('Successfully initialized new cluster. Any running PVC daemons will need to be restarted.')
|
Loading…
Reference in New Issue