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.network as pvc_network
|
||||
import client_lib.ceph as pvc_ceph
|
||||
import client_lib.provisioner as pvc_provisioner
|
||||
|
||||
myhostname = socket.gethostname()
|
||||
zk_host = ''
|
||||
|
@ -188,6 +189,56 @@ def cli_vm():
|
|||
"""
|
||||
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
|
||||
###############################################################################
|
||||
|
@ -1163,47 +1214,15 @@ def ceph_pool_list(limit):
|
|||
###############################################################################
|
||||
# pvc init
|
||||
###############################################################################
|
||||
|
||||
@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():
|
||||
"""
|
||||
Perform initialization of Zookeeper to act as a 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.
|
||||
Perform initialization of a new PVC cluster.
|
||||
"""
|
||||
|
||||
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.')
|
||||
import pvc_init
|
||||
pvc_init.run()
|
||||
|
||||
|
||||
###############################################################################
|
||||
|
@ -1256,6 +1275,7 @@ cli_node.add_command(node_unflush)
|
|||
cli_node.add_command(node_info)
|
||||
cli_node.add_command(node_list)
|
||||
|
||||
cli_vm.add_command(vm_add)
|
||||
cli_vm.add_command(vm_define)
|
||||
cli_vm.add_command(vm_modify)
|
||||
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