Joshua M. Boniface 8faa3bb53d Handle info fuzzy matches better
If we are calling info, we want one VM. Don't silently discard other
options or try (and fail later) to parse multiple, just say no VM found.
2020-04-09 10:26:49 -04:00

589 lines
22 KiB
Python

#!/usr/bin/env python3
# vm.py - PVC CLI client function library, VM functions
# Part of the Parallel Virtual Cluster (PVC) system
#
# Copyright (C) 2018-2020 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 time
import re
import subprocess
from collections import deque
import cli_lib.ansiprint as ansiprint
import cli_lib.ceph as ceph
from cli_lib.common import call_api
#
# Primary functions
#
def vm_info(config, vm):
"""
Get information about (single) VM
API endpoint: GET /api/v1/vm/{vm}
API arguments:
API schema: {json_data_object}
"""
response = call_api(config, 'get', '/vm/{vm}'.format(vm=vm))
if response.status_code == 200:
if len(response.json()) > 1:
# No exact match; return not found
return False, "VM not found."
else:
return True, response.json()[0]
return True, response.json()
else:
return False, response.json()['message']
def vm_list(config, limit, target_node, target_state):
"""
Get list information about VMs (limited by {limit}, {target_node}, or {target_state})
API endpoint: GET /api/v1/vm
API arguments: limit={limit}, node={target_node}, state={target_state}
API schema: [{json_data_object},{json_data_object},etc.]
"""
params = dict()
if limit:
params['limit'] = limit
if target_node:
params['node'] = target_node
if target_state:
params['state'] = target_state
response = call_api(config, 'get', '/vm', params=params)
if response.status_code == 200:
return True, response.json()
else:
return False, response.json()['message']
def vm_define(config, xml, node, node_limit, node_selector, node_autostart):
"""
Define a new VM on the cluster
API endpoint: POST /vm
API arguments: xml={xml}, node={node}, limit={node_limit}, selector={node_selector}, autostart={node_autostart}
API schema: {"message":"{data}"}
"""
params = {
'node': node,
'limit': node_limit,
'selector': node_selector,
'autostart': node_autostart
}
data = {
'xml': xml
}
response = call_api(config, 'post', '/vm', params=params, data=data)
if response.status_code == 200:
retstatus = True
else:
retstatus = False
return retstatus, response.json()['message']
def vm_modify(config, vm, xml, restart):
"""
Modify the configuration of VM
API endpoint: PUT /vm/{vm}
API arguments: xml={xml}, restart={restart}
API schema: {"message":"{data}"}
"""
params = {
'restart': restart
}
data = {
'xml': xml
}
response = call_api(config, 'put', '/vm/{vm}'.format(vm=vm), params=params, data=data)
if response.status_code == 200:
retstatus = True
else:
retstatus = False
return retstatus, response.json()['message']
def vm_metadata(config, vm, node_limit, node_selector, node_autostart, provisioner_profile):
"""
Modify PVC metadata of a VM
API endpoint: GET /vm/{vm}/meta, POST /vm/{vm}/meta
API arguments: limit={node_limit}, selector={node_selector}, autostart={node_autostart}, profile={provisioner_profile}
API schema: {"message":"{data}"}
"""
params = dict()
# Update any params that we've sent
if node_limit is not None:
params['limit'] = node_limit
if node_selector is not None:
params['selector'] = node_selector
if node_autostart is not None:
params['autostart'] = node_autostart
if provisioner_profile is not None:
params['profile'] = provisioner_profile
# Write the new metadata
response = call_api(config, 'post', '/vm/{vm}/meta'.format(vm=vm), params=params)
if response.status_code == 200:
retstatus = True
else:
retstatus = False
return retstatus, response.json()['message']
def vm_remove(config, vm, delete_disks=False):
"""
Remove a VM
API endpoint: DELETE /vm/{vm}
API arguments: delete_disks={delete_disks}
API schema: {"message":"{data}"}
"""
params={
'delete_disks': delete_disks
}
response = call_api(config, 'delete', '/vm/{vm}'.format(vm=vm), params=params)
if response.status_code == 200:
retstatus = True
else:
retstatus = False
return retstatus, response.json()['message']
def vm_state(config, vm, target_state, wait=False):
"""
Modify the current state of VM
API endpoint: POST /vm/{vm}/state
API arguments: state={state}, wait={wait}
API schema: {"message":"{data}"}
"""
params={
'state': target_state,
'wait': str(wait).lower()
}
response = call_api(config, 'post', '/vm/{vm}/state'.format(vm=vm), params=params)
if response.status_code == 200:
retstatus = True
else:
retstatus = False
return retstatus, response.json()['message']
def vm_node(config, vm, target_node, action, force=False, wait=False):
"""
Modify the current node of VM via {action}
API endpoint: POST /vm/{vm}/node
API arguments: node={target_node}, action={action}, force={force}, wait={wait}
API schema: {"message":"{data}"}
"""
params={
'node': target_node,
'action': action,
'force': str(force).lower(),
'wait': str(wait).lower()
}
response = call_api(config, 'post', '/vm/{vm}/node'.format(vm=vm), params=params)
if response.status_code == 200:
retstatus = True
else:
retstatus = False
return retstatus, response.json()['message']
def vm_locks(config, vm):
"""
Flush RBD locks of (stopped) VM
API endpoint: POST /vm/{vm}/locks
API arguments:
API schema: {"message":"{data}"}
"""
response = call_api(config, 'post', '/vm/{vm}/locks'.format(vm=vm))
if response.status_code == 200:
retstatus = True
else:
retstatus = False
return retstatus, response.json()['message']
def view_console_log(config, vm, lines=100):
"""
Return console log lines from the API (and display them in a pager in the main CLI)
API endpoint: GET /vm/{vm}/console
API arguments: lines={lines}
API schema: {"name":"{vmname}","data":"{console_log}"}
"""
params = {
'lines': lines
}
response = call_api(config, 'get', '/vm/{vm}/console'.format(vm=vm), params=params)
if response.status_code != 200:
return False, response.json()['message']
console_log = response.json()['data']
# Shrink the log buffer to length lines
shrunk_log = console_log.split('\n')[-lines:]
loglines = '\n'.join(shrunk_log)
return True, loglines
def follow_console_log(config, vm, lines=10):
"""
Return and follow console log lines from the API
API endpoint: GET /vm/{vm}/console
API arguments: lines={lines}
API schema: {"name":"{vmname}","data":"{console_log}"}
"""
params = {
'lines': lines
}
response = call_api(config, 'get', '/vm/{vm}/console'.format(vm=vm), params=params)
if response.status_code != 200:
return False, response.json()['message']
# Shrink the log buffer to length lines
console_log = response.json()['data']
shrunk_log = console_log.split('\n')[-lines:]
loglines = '\n'.join(shrunk_log)
# Print the initial data and begin following
print(loglines, end='')
while True:
# Grab the next line set
try:
response = call_api(config, 'get', '/vm/{vm}/console'.format(vm=vm), params=params)
new_console_log = response.json()['data']
except:
break
# Split the new and old log strings into constitutent lines
old_console_loglines = console_log.split('\n')
new_console_loglines = new_console_log.split('\n')
# Set the console log to the new log value for the next iteration
console_log = new_console_log
# Remove the lines from the old log until we hit the first line of the new log; this
# ensures that the old log is a string that we can remove from the new log entirely
for index, line in enumerate(old_console_loglines, start=0):
if line == new_console_loglines[0]:
del old_console_loglines[0:index]
break
# Rejoin the log lines into strings
old_console_log = '\n'.join(old_console_loglines)
new_console_log = '\n'.join(new_console_loglines)
# Remove the old lines from the new log
diff_console_log = new_console_log.replace(old_console_log, "")
# If there's a difference, print it out
if diff_console_log:
print(diff_console_log, end='')
# Wait a second
time.sleep(1)
return True, ''
#
# Output display functions
#
def format_info(config, domain_information, long_output):
# Format a nice output; do this line-by-line then concat the elements at the end
ainformation = []
ainformation.append('{}Virtual machine information:{}'.format(ansiprint.bold(), ansiprint.end()))
ainformation.append('')
# Basic information
ainformation.append('{}UUID:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['uuid']))
ainformation.append('{}Name:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['name']))
ainformation.append('{}Description:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['description']))
ainformation.append('{}Profile:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['profile']))
ainformation.append('{}Memory (M):{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['memory']))
ainformation.append('{}vCPUs:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['vcpu']))
ainformation.append('{}Topology (S/C/T):{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['vcpu_topology']))
if long_output == True:
# Virtualization information
ainformation.append('')
ainformation.append('{}Emulator:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['emulator']))
ainformation.append('{}Type:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['type']))
ainformation.append('{}Arch:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['arch']))
ainformation.append('{}Machine:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['machine']))
ainformation.append('{}Features:{} {}'.format(ansiprint.purple(), ansiprint.end(), ' '.join(domain_information['features'])))
# PVC cluster information
ainformation.append('')
dstate_colour = {
'start': ansiprint.green(),
'restart': ansiprint.yellow(),
'shutdown': ansiprint.yellow(),
'stop': ansiprint.red(),
'disable': ansiprint.blue(),
'fail': ansiprint.red(),
'migrate': ansiprint.blue(),
'unmigrate': ansiprint.blue(),
'provision': ansiprint.blue()
}
ainformation.append('{}State:{} {}{}{}'.format(ansiprint.purple(), ansiprint.end(), dstate_colour[domain_information['state']], domain_information['state'], ansiprint.end()))
ainformation.append('{}Current Node:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['node']))
if not domain_information['last_node']:
domain_information['last_node'] = "N/A"
ainformation.append('{}Previous Node:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['last_node']))
# Get a failure reason if applicable
if domain_information['failed_reason']:
ainformation.append('')
ainformation.append('{}Failure reason:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['failed_reason']))
if not domain_information['node_selector']:
formatted_node_selector = "False"
else:
formatted_node_selector = domain_information['node_selector']
if not domain_information['node_limit']:
formatted_node_limit = "False"
else:
formatted_node_limit = ', '.join(domain_information['node_limit'])
if not domain_information['node_autostart']:
formatted_node_autostart = "False"
else:
formatted_node_autostart = domain_information['node_autostart']
ainformation.append('{}Migration selector:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_node_selector))
ainformation.append('{}Node limit:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_node_limit))
ainformation.append('{}Autostart:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_node_autostart))
# Network list
net_list = []
for net in domain_information['networks']:
# Split out just the numerical (VNI) part of the brXXXX name
net_vnis = re.findall(r'\d+', net['source'])
if net_vnis:
net_vni = net_vnis[0]
else:
net_vni = re.sub('br', '', net['source'])
response = call_api(config, 'get', '/network/{net}'.format(net=net_vni))
if response.status_code != 200 and net_vni != 'cluster':
net_list.append(ansiprint.red() + net_vni + ansiprint.end() + ' [invalid]')
else:
net_list.append(net_vni)
ainformation.append('')
ainformation.append('{}Networks:{} {}'.format(ansiprint.purple(), ansiprint.end(), ', '.join(net_list)))
if long_output == True:
# Disk list
ainformation.append('')
name_length = 0
for disk in domain_information['disks']:
_name_length = len(disk['name']) + 1
if _name_length > name_length:
name_length = _name_length
ainformation.append('{0}Disks:{1} {2}ID Type {3: <{width}} Dev Bus{4}'.format(ansiprint.purple(), ansiprint.end(), ansiprint.bold(), 'Name', ansiprint.end(), width=name_length))
for disk in domain_information['disks']:
ainformation.append(' {0: <3} {1: <5} {2: <{width}} {3: <4} {4: <5}'.format(domain_information['disks'].index(disk), disk['type'], disk['name'], disk['dev'], disk['bus'], width=name_length))
ainformation.append('')
ainformation.append('{}Interfaces:{} {}ID Type Source Model MAC{}'.format(ansiprint.purple(), ansiprint.end(), ansiprint.bold(), ansiprint.end()))
for net in domain_information['networks']:
ainformation.append(' {0: <3} {1: <8} {2: <10} {3: <8} {4}'.format(domain_information['networks'].index(net), net['type'], net['source'], net['model'], net['mac']))
# Controller list
ainformation.append('')
ainformation.append('{}Controllers:{} {}ID Type Model{}'.format(ansiprint.purple(), ansiprint.end(), ansiprint.bold(), ansiprint.end()))
for controller in domain_information['controllers']:
ainformation.append(' {0: <3} {1: <14} {2: <8}'.format(domain_information['controllers'].index(controller), controller['type'], controller['model']))
# Join it all together
ainformation.append('')
return '\n'.join(ainformation)
def format_list(config, vm_list, raw):
# Handle single-element lists
if not isinstance(vm_list, list):
vm_list = [ vm_list ]
# Function to strip the "br" off of nets and return a nicer list
def getNiceNetID(domain_information):
# Network list
net_list = []
for net in domain_information['networks']:
# Split out just the numerical (VNI) part of the brXXXX name
net_vnis = re.findall(r'\d+', net['source'])
if net_vnis:
net_vni = net_vnis[0]
else:
net_vni = re.sub('br', '', net['source'])
net_list.append(net_vni)
return net_list
# Handle raw mode since it just lists the names
if raw:
ainformation = list()
for vm in sorted(item['name'] for item in vm_list):
ainformation.append(vm)
return '\n'.join(ainformation)
vm_list_output = []
# Determine optimal column widths
# Dynamic columns: node_name, node, migrated
vm_name_length = 5
vm_uuid_length = 37
vm_state_length = 6
vm_nets_length = 9
vm_ram_length = 8
vm_vcpu_length = 6
vm_node_length = 8
vm_migrated_length = 10
for domain_information in vm_list:
net_list = getNiceNetID(domain_information)
# vm_name column
_vm_name_length = len(domain_information['name']) + 1
if _vm_name_length > vm_name_length:
vm_name_length = _vm_name_length
# vm_state column
_vm_state_length = len(domain_information['state']) + 1
if _vm_state_length > vm_state_length:
vm_state_length = _vm_state_length
# vm_nets column
_vm_nets_length = len(','.join(net_list)) + 1
if _vm_nets_length > vm_nets_length:
vm_nets_length = _vm_nets_length
# vm_node column
_vm_node_length = len(domain_information['node']) + 1
if _vm_node_length > vm_node_length:
vm_node_length = _vm_node_length
# vm_migrated column
_vm_migrated_length = len(domain_information['migrated']) + 1
if _vm_migrated_length > vm_migrated_length:
vm_migrated_length = _vm_migrated_length
# Format the string (header)
vm_list_output.append(
'{bold}{vm_name: <{vm_name_length}} {vm_uuid: <{vm_uuid_length}} \
{vm_state_colour}{vm_state: <{vm_state_length}}{end_colour} \
{vm_networks: <{vm_nets_length}} \
{vm_memory: <{vm_ram_length}} {vm_vcpu: <{vm_vcpu_length}} \
{vm_node: <{vm_node_length}} \
{vm_migrated: <{vm_migrated_length}}{end_bold}'.format(
vm_name_length=vm_name_length,
vm_uuid_length=vm_uuid_length,
vm_state_length=vm_state_length,
vm_nets_length=vm_nets_length,
vm_ram_length=vm_ram_length,
vm_vcpu_length=vm_vcpu_length,
vm_node_length=vm_node_length,
vm_migrated_length=vm_migrated_length,
bold=ansiprint.bold(),
end_bold=ansiprint.end(),
vm_state_colour='',
end_colour='',
vm_name='Name',
vm_uuid='UUID',
vm_state='State',
vm_networks='Networks',
vm_memory='RAM (M)',
vm_vcpu='vCPUs',
vm_node='Node',
vm_migrated='Migrated'
)
)
# Keep track of nets we found to be valid to cut down on duplicate API hits
valid_net_list = []
# Format the string (elements)
for domain_information in vm_list:
if domain_information['state'] == 'start':
vm_state_colour = ansiprint.green()
elif domain_information['state'] == 'restart':
vm_state_colour = ansiprint.yellow()
elif domain_information['state'] == 'shutdown':
vm_state_colour = ansiprint.yellow()
elif domain_information['state'] == 'stop':
vm_state_colour = ansiprint.red()
elif domain_information['state'] == 'fail':
vm_state_colour = ansiprint.red()
else:
vm_state_colour = ansiprint.blue()
# Handle colouring for an invalid network config
raw_net_list = getNiceNetID(domain_information)
net_list = []
vm_net_colour = ''
for net_vni in raw_net_list:
if not net_vni in valid_net_list:
response = call_api(config, 'get', '/network/{net}'.format(net=net_vni))
if response.status_code != 200 and net_vni != 'cluster':
vm_net_colour = ansiprint.red()
else:
valid_net_list.append(net_vni)
net_list.append(net_vni)
vm_list_output.append(
'{bold}{vm_name: <{vm_name_length}} {vm_uuid: <{vm_uuid_length}} \
{vm_state_colour}{vm_state: <{vm_state_length}}{end_colour} \
{vm_net_colour}{vm_networks: <{vm_nets_length}}{end_colour} \
{vm_memory: <{vm_ram_length}} {vm_vcpu: <{vm_vcpu_length}} \
{vm_node: <{vm_node_length}} \
{vm_migrated: <{vm_migrated_length}}{end_bold}'.format(
vm_name_length=vm_name_length,
vm_uuid_length=vm_uuid_length,
vm_state_length=vm_state_length,
vm_nets_length=vm_nets_length,
vm_ram_length=vm_ram_length,
vm_vcpu_length=vm_vcpu_length,
vm_node_length=vm_node_length,
vm_migrated_length=vm_migrated_length,
bold='',
end_bold='',
vm_state_colour=vm_state_colour,
end_colour=ansiprint.end(),
vm_name=domain_information['name'],
vm_uuid=domain_information['uuid'],
vm_state=domain_information['state'],
vm_net_colour=vm_net_colour,
vm_networks=','.join(net_list),
vm_memory=domain_information['memory'],
vm_vcpu=domain_information['vcpu'],
vm_node=domain_information['node'],
vm_migrated=domain_information['migrated']
)
)
return '\n'.join(sorted(vm_list_output))