# 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))