fuel-devops/devops/shell.py

769 lines
34 KiB
Python

# Copyright 2013 - 2016 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import print_function
import argparse
import collections
import os
import sys
import datetime
import tabulate
from six.moves import input
import devops
from devops import client
from devops import error
from devops.helpers import helpers
from devops import logger
class Shell(object):
def __init__(self, args):
self.args = args
self.params = self.get_params()
self.client = client.DevopsClient()
self.env = None
name = getattr(self.params, 'name', None)
command = getattr(self.params, 'command', None)
if name and command != 'create':
self.env = self.client.get_env(name)
def execute(self):
command_name = 'do_{}'.format(self.params.command.replace('-', '_'))
command_method = getattr(self, command_name)
command_method()
@staticmethod
def print_table(headers, columns):
if not columns:
return
print(tabulate.tabulate(columns, headers=headers,
tablefmt="simple"))
@staticmethod
def query_yes_no(question, default=None):
"""Ask a yes/no question via standard input and return the answer.
If invalid input is given, the user will be asked until
they acutally give valid input.
Args:
question(str):
A question that is presented to the user.
default(bool|None):
The default value when enter is pressed with no value.
When None, there is no default value and the query
will loop.
Returns:
A bool indicating whether user has entered yes or no.
Side Effects:
Blocks program execution until valid input(y/n) is given.
"""
yes_list = ["yes", "y"]
no_list = ["no", "n"]
default_dict = { # default => prompt default string
None: "[y/n]",
True: "[Y/n]",
False: "[y/N]",
}
default_str = default_dict[default]
prompt_str = "{} {} ".format(question, default_str)
while True:
choice = input(prompt_str).lower()
if not choice and default is not None:
return default
if choice in yes_list:
return True
if choice in no_list:
return False
notification_str = "Please respond with 'y' or 'n'"
print(notification_str)
def print_envs_table(self, env_names_list):
columns = []
for env_name in sorted(env_names_list):
env = self.client.get_env(env_name)
column = collections.OrderedDict()
column['NAME'] = env.name
if self.params.list_ips:
if env.has_admin():
column['ADMIN IP'] = env.get_admin_ip()
else:
column['ADMIN IP'] = ''
if self.params.timestamps:
column['CREATED'] = helpers.utc_to_local(env.created).strftime(
'%Y-%m-%d_%H:%M:%S')
columns.append(column)
self.print_table(headers='keys', columns=columns)
def do_list(self):
self.print_envs_table(self.client.list_env_names())
def do_show(self):
nodes = sorted(self.env.get_nodes(), key=lambda node: node.name)
headers = ("VNC", "NODE-NAME", "GROUP-NAME")
columns = [(node.get_vnc_port(), node.name, node.group.name)
for node in nodes]
self.print_table(headers=headers, columns=columns)
def do_show_resources(self):
nodes = sorted(self.env.get_nodes(), key=lambda node: node.name)
headers = ("NAME", "ROLE", "GROUP", "VCPU", "MEMORY,Gb", "STORAGE,Gb")
total_vcpu = 0
total_memory = 0
total_storage = 0
columns = list()
for node in nodes:
vcpu = 0
memory = 0
storage = 0
if 'vcpu' in node.get_defined_params():
vcpu = node.vcpu
if 'memory' in node.get_defined_params():
memory = node.memory
volumes = node.get_volumes()
for volume in volumes:
if 'capacity' in volume.get_defined_params():
storage += int(volume.capacity)
columns.append(
(
node.name,
node.role,
node.group.name,
vcpu or '-',
memory or '-',
storage or '-',
)
)
total_vcpu += vcpu
total_memory += memory
total_storage += storage
columns.append(
(
"Total:",
'',
'',
total_vcpu or '-',
total_memory or '-',
total_storage or '-',
)
)
self.print_table(headers=headers, columns=columns)
def do_erase(self):
self.env.erase()
def get_lifetime_delta(self):
data = self.params.env_lifetime
multipliers = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}
if data[-1] not in multipliers:
raise ValueError(
'Value should end with '
'one of "{}", got "{}"'.format(
" ".join(multipliers.keys()), data
))
num = int(data[:-1])
mul = data[-1]
return datetime.timedelta(seconds=num*multipliers[mul])
def get_old_environments(self):
delta = self.get_lifetime_delta()
# devops uses utc timestamps for BaseModel
timestamp_now = datetime.datetime.utcnow()
envs_to_erase = []
for env_name in client.DevopsClient.list_env_names():
env = client.DevopsClient.get_env(env_name)
if (timestamp_now - env.created) > delta:
envs_to_erase.append(env)
return envs_to_erase
def do_erase_old(self):
envs_to_erase = self.get_old_environments()
for env in envs_to_erase:
print("Env '{}' will be erased!".format(env.name))
if envs_to_erase:
if not self.params.force_cleanup:
answer = self.query_yes_no(
"The cleanup operation is destructive one, "
"all environments listed above will be erased. "
"DELETION CAN NOT BE UNDONE! Proceed? ",
default=False)
if not answer:
print("Wise choice, aborting...")
sys.exit(0)
else:
print("Nothing to erase, exiting...")
sys.exit(0)
for env in envs_to_erase:
print("Erasing '{}'...".format(env.name))
env.erase()
def do_list_old(self):
env_names = [env.name for env in self.get_old_environments()]
self.print_envs_table(env_names)
def do_start(self):
self.env.start()
def do_destroy(self):
self.env.destroy()
def do_suspend(self):
self.env.suspend()
def do_resume(self):
self.env.resume()
def do_revert(self):
self.env.revert(self.params.snapshot_name, flag=False, resume=False)
def do_snapshot(self):
self.env.snapshot(self.params.snapshot_name)
def do_sync(self):
self.client.synchronize_all()
def do_snapshot_list(self):
snapshots = collections.OrderedDict()
# noinspection PyPep8Naming
Snap = collections.namedtuple('Snap', ['info', 'nodes'])
for node in self.env.get_nodes():
for snap in node.get_snapshots():
if snap.name in snapshots:
snapshots[snap.name].nodes.append(node.name)
else:
snapshots[snap.name] = Snap(snap, [node.name, ])
snapshots = sorted(snapshots.values(), key=lambda x: x.info.created)
headers = ('SNAPSHOT', 'CREATED', 'NODES-NAMES')
columns = []
for info, nodes in snapshots:
nodes.sort()
columns.append((
info.name,
helpers.utc_to_local(
info.created).strftime('%Y-%m-%d %H:%M:%S'),
', '.join(nodes),
))
self.print_table(columns=columns, headers=headers)
def do_snapshot_delete(self):
for node in self.env.get_nodes():
snaps = [x.name for x in node.get_snapshots()]
if self.params.snapshot_name in snaps:
node.erase_snapshot(name=self.params.snapshot_name)
def do_net_list(self):
headers = ("NETWORK NAME", "IP NET")
columns = [(net.name, net.ip_network)
for net in self.env.get_address_pools()]
self.print_table(headers=headers, columns=columns)
def do_slave_ip_list(self):
address_pool_name = self.params.address_pool_name
slave_ips = {}
for l2dev in self.env.get_env_l2_network_devices():
if l2dev.address_pool is None:
continue
if address_pool_name and \
l2dev.address_pool.name != address_pool_name:
continue
ap_slave_ips = []
for node in self.env.get_nodes():
try:
node.get_interface_by_network_name(l2dev.name)
except devops.models.network.Interface.DoesNotExist:
# Skip if l2 network device is not attached to the node
continue
if self.params.ip_only:
ap_slave_ips.append(
node.get_ip_address_by_network_name(l2dev.name))
else:
ap_slave_ips.append(
"{0},{1}".format(
node.name,
node.get_ip_address_by_network_name(l2dev.name)))
if ap_slave_ips:
slave_ips[l2dev.address_pool.name] = ap_slave_ips
if not slave_ips:
sys.exit('No IPs were allocated for environment!')
for ap, n_ips in sorted(slave_ips.items()):
if address_pool_name:
print(' '.join(n_ips))
else:
print(ap + ": " + ' '.join(n_ips))
def do_time_sync(self):
node_name = self.params.node_name
node_names = [node_name] if node_name else None
cur_time = self.env.get_curr_time(node_names)
for name in sorted(cur_time):
print('Current time on {0!r} = {1}'.format(name, cur_time[name]))
print('Please wait for a few minutes while time is synchronized...')
new_time = self.env.sync_time(node_names)
for name in sorted(new_time):
print("New time on '{0}' = {1}".format(name, new_time[name]))
def do_revert_resume(self):
self.env.revert(self.params.snapshot_name, flag=False, resume=True)
if self.params.timesync:
print('Time synchronization is starting')
self.do_time_sync()
@staticmethod
def do_version():
print(devops.__version__)
def do_create(self):
"""Create env using cli parameters."""
env = self.client.create_env(
env_name=self.params.name,
admin_iso_path=self.params.iso_path,
admin_vcpu=self.params.admin_vcpu_count,
admin_memory=self.params.admin_ram_size,
admin_sysvolume_capacity=self.params.admin_disk_size,
nodes_count=self.params.node_count,
slave_vcpu=self.params.vcpu_count,
slave_memory=self.params.ram_size,
second_volume_capacity=self.params.second_disk_size,
third_volume_capacity=self.params.third_disk_size,
net_pool=self.params.net_pool.split(':'),
)
env.define()
def do_create_env(self):
"""Create env using config file."""
env = self.client.create_env_from_config(
self.params.env_config_name)
env.define()
def do_slave_add(self):
self.env.add_slaves(
nodes_count=self.params.node_count,
slave_vcpu=self.params.vcpu_count,
slave_memory=self.params.ram_size,
second_volume_capacity=self.params.second_disk_size,
third_volume_capacity=self.params.third_disk_size,
)
def do_slave_remove(self):
# TODO(astudenov): add positional argument instead of option
node = self.env.get_node(name=self.params.node_name)
node.remove()
def do_slave_change(self):
node = self.env.get_node(name=self.params.node_name)
# TODO(astudenov): check if node is under libvirt controll
node.set_vcpu(vcpu=self.params.vcpu_count)
node.set_memory(memory=self.params.ram_size)
def do_admin_change(self):
node = self.env.get_node(name="admin")
# TODO(astudenov): check if node is under libvirt controll
node.set_vcpu(vcpu=self.params.admin_vcpu_count)
node.set_memory(memory=self.params.admin_ram_size)
def do_admin_setup(self):
# start networks first
for group in self.env.get_groups():
group.start_networks()
self.env.admin_setup(
boot_from=self.params.boot_from,
iface=self.params.iface)
print('Setup complete.\n ssh {0}@{1}'.format(
self.env.get_admin_login(),
self.env.get_admin_ip()))
def do_node_start(self):
# TODO(astudenov): add positional argument instead of
# checking that option is present
self.check_param_show_help(self.params.node_name)
self.env.get_node(name=self.params.node_name).start()
def do_node_destroy(self):
# TODO(astudenov): add positional argument instead of
# checking that option is present
self.check_param_show_help(self.params.node_name)
self.env.get_node(name=self.params.node_name).destroy()
def do_node_reset(self):
# TODO(astudenov): add positional argument instead of
# checking that option is present
self.check_param_show_help(self.params.node_name)
self.env.get_node(name=self.params.node_name).reset()
def check_param_show_help(self, parameter):
if not parameter:
self.args.append('-h')
self.get_params()
def get_params(self):
name_parser = argparse.ArgumentParser(add_help=False)
name_parser.add_argument('name', help='environment name',
default=os.environ.get('ENV_NAME'),
metavar='ENV_NAME')
group_name_parser = argparse.ArgumentParser(add_help=False)
group_name_parser.add_argument('--group-name', help='group name',
default='default')
env_config_name_parser = argparse.ArgumentParser(add_help=False)
env_config_name_parser.add_argument('env_config_name',
help='environment template name',
default=os.environ.get(
'DEVOPS_SETTINGS_TEMPLATE'))
snapshot_name_parser = argparse.ArgumentParser(add_help=False)
snapshot_name_parser.add_argument('snapshot_name',
help='snapshot name',
default=os.environ.get(
'SNAPSHOT_NAME'))
node_name_parser = argparse.ArgumentParser(add_help=False)
node_name_parser.add_argument('--node-name', '-N',
help='node name',
default=None)
timesync_parser = argparse.ArgumentParser(add_help=False)
timesync_parser.add_argument('--timesync', dest='timesync',
action='store_const', const=True,
help='revert with timesync',
default=False)
list_ips_parser = argparse.ArgumentParser(add_help=False)
list_ips_parser.add_argument('--ips', dest='list_ips',
action='store_const', const=True,
help='show admin node ip addresses',
default=False)
timestamps_parser = argparse.ArgumentParser(add_help=False)
timestamps_parser.add_argument('--timestamps', dest='timestamps',
action='store_const', const=True,
help='show creation timestamps',
default=False)
iso_path_parser = argparse.ArgumentParser(add_help=False)
iso_path_parser.add_argument('--iso-path', '-I', dest='iso_path',
help='Set Fuel ISO path',
required=True)
admin_ram_parser = argparse.ArgumentParser(add_help=False)
admin_ram_parser.add_argument('--admin-ram', dest='admin_ram_size',
help='Select admin node RAM size (MB)',
default=1536, type=int)
admin_vcpu_parser = argparse.ArgumentParser(add_help=False)
admin_vcpu_parser.add_argument('--admin-vcpu', dest='admin_vcpu_count',
help='Select admin node VCPU count',
default=2, type=int)
change_admin_ram_parser = argparse.ArgumentParser(add_help=False)
change_admin_ram_parser.add_argument('--admin-ram',
dest='admin_ram_size',
help='Select admin node RAM '
'size (MB)',
default=None, type=int)
change_admin_vcpu_parser = argparse.ArgumentParser(add_help=False)
change_admin_vcpu_parser.add_argument('--admin-vcpu',
dest='admin_vcpu_count',
help='Select admin node VCPU '
'count',
default=None, type=int)
admin_disk_size_parser = argparse.ArgumentParser(add_help=False)
admin_disk_size_parser.add_argument('--admin-disk-size',
dest='admin_disk_size',
help='Set admin node disk '
'size (GB)',
default=50, type=int)
admin_setup_iface_parser = argparse.ArgumentParser(add_help=False)
admin_setup_iface_parser.add_argument('--iface',
dest='iface',
help='Static network interface '
'to use when configuring '
'the admin node. Should '
'be eth0 or enp0s3',
default='enp0s3')
admin_setup_boot_from_parser = argparse.ArgumentParser(add_help=False)
admin_setup_boot_from_parser.add_argument(
'--boot-from', dest='boot_from', default='cdrom',
help='Set device to boot from for admin node. '
'Should be cdrom or usb')
ram_parser = argparse.ArgumentParser(add_help=False)
ram_parser.add_argument('--ram', dest='ram_size',
help='Set node RAM size',
default=1024, type=int)
vcpu_parser = argparse.ArgumentParser(add_help=False)
vcpu_parser.add_argument('--vcpu', dest='vcpu_count',
help='Set node VCPU count',
default=1, type=int)
change_ram_parser = argparse.ArgumentParser(add_help=False)
change_ram_parser.add_argument('--ram', dest='ram_size',
help='Set node RAM size',
default=None, type=int)
change_vcpu_parser = argparse.ArgumentParser(add_help=False)
change_vcpu_parser.add_argument('--vcpu', dest='vcpu_count',
help='Set node VCPU count',
default=None, type=int)
node_count = argparse.ArgumentParser(add_help=False)
node_count.add_argument('--node-count', '-C', dest='node_count',
help='How many nodes will be created',
default=1, type=int)
net_pool = argparse.ArgumentParser(add_help=False)
net_pool.add_argument('--net-pool', '-P', dest='net_pool',
help='Set ip network pool (cidr)',
default="10.21.0.0/16:24", type=str)
address_pool_name = argparse.ArgumentParser(add_help=False)
address_pool_name.add_argument(
'--address-pool-name', '-A',
dest='address_pool_name',
help='Specified address pool for printing IPs',
default=None, type=str)
ip_only_parser = argparse.ArgumentParser(add_help=False)
ip_only_parser.add_argument('--ip-only', dest='ip_only',
action='store_const', const=True,
help='Print just IP addresses',
default=False)
second_disk_size = argparse.ArgumentParser(add_help=False)
second_disk_size.add_argument('--second-disk-size',
dest='second_disk_size',
help='Allocate second disk for node '
'with selected size(GB). '
'If set to 0, the disk will not be '
'allocated',
default=50, type=int)
third_disk_size = argparse.ArgumentParser(add_help=False)
third_disk_size.add_argument('--third-disk-size',
dest='third_disk_size',
help='Allocate the third disk for node '
'with selected size(GB). '
'If set to 0, the disk will not be '
'allocated',
default=50, type=int)
force_cleanup_parser = argparse.ArgumentParser(add_help=False)
force_cleanup_parser.add_argument(
'--force-cleanup',
dest='force_cleanup',
action='store_const', const=True,
help='Do not ask confirmation for cleanup action.',
default=False)
env_lifetime = argparse.ArgumentParser(add_help=False)
env_lifetime.add_argument(
dest='env_lifetime',
help='Erase environments older than given time interval. '
'Example:"45m", "12h", "3d"',
default="", type=str)
parser = argparse.ArgumentParser(
description="Manage virtual environments. "
"For additional help, use with -h/--help option")
subparsers = parser.add_subparsers(title="Operation commands",
help='available commands',
dest='command')
subparsers.add_parser('list',
parents=[list_ips_parser, timestamps_parser],
help="Show virtual environments",
description="Show virtual environments on host")
subparsers.add_parser('erase-old',
parents=[force_cleanup_parser,
env_lifetime],
help="Cleanup old virtual environments",
description="Cleanup virtual environments on "
"host")
subparsers.add_parser('list-old',
parents=[env_lifetime, list_ips_parser,
timestamps_parser],
help="Show virtual environments older than given"
" lifetime interval",
description="Show old virtual "
"environments on host")
subparsers.add_parser('show', parents=[name_parser],
help="Show VMs in environment",
description="Show VMs in environment")
subparsers.add_parser('show-resources', parents=[name_parser],
help=("Show resources consumed by VMs "
"in environment"),
description=("Show resources consumed by VMs "
"in environment"))
subparsers.add_parser('erase', parents=[name_parser],
help="Delete environment",
description="Delete environment and VMs on it")
subparsers.add_parser('start', parents=[name_parser],
help="Start VMs",
description="Start VMs in selected environment")
subparsers.add_parser('destroy', parents=[name_parser],
help="Destroy(stop) VMs",
description="Stop VMs in selected environment")
subparsers.add_parser('suspend', parents=[name_parser],
help="Suspend VMs",
description="Suspend VMs in selected "
"environment")
subparsers.add_parser('resume', parents=[name_parser],
help="Resume VMs",
description="Resume VMs in selected environment")
subparsers.add_parser('revert',
parents=[name_parser, snapshot_name_parser],
help="Apply snapshot to environment",
description="Apply selected snapshot to "
"environment")
subparsers.add_parser('snapshot',
parents=[name_parser, snapshot_name_parser],
help="Make environment snapshot",
description="Make environment snapshot")
subparsers.add_parser('sync',
help="Synchronization environment and devops",
description="Synchronization environment "
"and devops"),
subparsers.add_parser('snapshot-list',
parents=[name_parser],
help="Show snapshots in environment",
description="Show snapshots in selected "
"environment")
subparsers.add_parser('snapshot-delete',
parents=[name_parser, snapshot_name_parser],
help="Delete snapshot from environment",
description="Delete snapshot from selected "
"environment")
subparsers.add_parser('net-list',
parents=[name_parser],
help="Show networks in environment",
description="Display allocated networks for "
"environment")
subparsers.add_parser('slave-ip-list',
parents=[name_parser,
address_pool_name,
ip_only_parser],
help="Show slave node IPs in environment",
description="Display allocated IPs for "
"environment slave nodes")
subparsers.add_parser('time-sync',
parents=[name_parser, node_name_parser],
help="Sync time on all env nodes",
description="Sync time on all active nodes "
"of environment starting from "
"admin")
subparsers.add_parser('revert-resume',
parents=[name_parser, snapshot_name_parser,
node_name_parser, timesync_parser],
help="Revert, resume, sync time on VMs",
description="Revert and resume VMs in selected"
"environment, then optionally sync "
"time on VMs (by default time is "
"not synced, additional '--timesync'"
" flag is required)")
subparsers.add_parser('version',
help="Show devops version")
subparsers.add_parser('create',
parents=[name_parser, vcpu_parser,
node_count, ram_parser,
net_pool, iso_path_parser,
admin_disk_size_parser,
admin_ram_parser,
admin_vcpu_parser,
second_disk_size,
third_disk_size],
help="Create a new environment (DEPRECATED)",
description="Create an environment by using "
"cli options"),
subparsers.add_parser('create-env',
parents=[env_config_name_parser],
help="Create a new environment",
description="Create an environment from a "
"template file"),
subparsers.add_parser('slave-add',
parents=[name_parser, node_count,
ram_parser, vcpu_parser,
second_disk_size, third_disk_size,
group_name_parser],
help="Add a node",
description="Add a new node to environment")
subparsers.add_parser('slave-change',
parents=[name_parser, node_name_parser,
change_ram_parser, change_vcpu_parser],
help="Change node VCPU and memory config",
description="Change count of VCPUs and memory")
subparsers.add_parser('slave-remove',
parents=[name_parser, node_name_parser],
help="Remove node from environment",
description="Remove selected node from "
"environment")
subparsers.add_parser('admin-setup',
parents=[name_parser, admin_setup_iface_parser,
admin_setup_boot_from_parser],
help="Setup admin node",
description="Setup admin node from ISO")
subparsers.add_parser('admin-change',
parents=[name_parser, change_admin_ram_parser,
change_admin_vcpu_parser],
help="Change admin node VCPU and memory config",
description="Change count of VCPUs and memory "
"for admin node")
subparsers.add_parser('node-start',
parents=[name_parser, node_name_parser],
help="Start node in environment",
description="Start a separate node in "
"environment")
subparsers.add_parser('node-destroy',
parents=[name_parser, node_name_parser],
help="Destroy (power off) node in environment",
description="Destroy a separate node in "
"environment")
subparsers.add_parser('node-reset',
parents=[name_parser, node_name_parser],
help="Reset (restart) node in environment",
description="Reset a separate node in "
"environment")
if len(self.args) == 0:
self.args = ['-h']
return parser.parse_args(self.args)
def main(args=None):
if args is None:
args = sys.argv[1:]
try:
shell = Shell(args)
shell.execute()
except error.DevopsError as exc:
logger.debug(exc, exc_info=True)
sys.exit('Error: {}'.format(exc))