1213 lines
48 KiB
Python
Executable File
1213 lines
48 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
|
|
# Copyright 2010 United States Government as represented by the
|
|
# Administrator of the National Aeronautics and Space Administration.
|
|
# All Rights Reserved.
|
|
#
|
|
# 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.
|
|
|
|
# Interactive shell based on Django:
|
|
#
|
|
# Copyright (c) 2005, the Lawrence Journal-World
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are met:
|
|
#
|
|
# 1. Redistributions of source code must retain the above copyright notice,
|
|
# this list of conditions and the following disclaimer.
|
|
#
|
|
# 2. Redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in the
|
|
# documentation and/or other materials provided with the distribution.
|
|
#
|
|
# 3. Neither the name of Django nor the names of its contributors may be
|
|
# used to endorse or promote products derived from this software without
|
|
# specific prior written permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
|
|
"""
|
|
CLI interface for nova management.
|
|
"""
|
|
|
|
import gettext
|
|
import netaddr
|
|
import os
|
|
import sys
|
|
|
|
# If ../nova/__init__.py exists, add ../ to Python search path, so that
|
|
# it will override what happens to be installed in /usr/(local/)lib/python...
|
|
POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
|
os.pardir,
|
|
os.pardir))
|
|
if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'nova', '__init__.py')):
|
|
sys.path.insert(0, POSSIBLE_TOPDIR)
|
|
|
|
gettext.install('nova', unicode=1)
|
|
|
|
from nova.api.ec2 import ec2utils
|
|
from nova import availability_zones
|
|
from nova.compute import instance_types
|
|
from nova.compute import rpcapi as compute_rpcapi
|
|
from nova import config
|
|
from nova import context
|
|
from nova import db
|
|
from nova.db import migration
|
|
from nova import exception
|
|
from nova.openstack.common import cfg
|
|
from nova.openstack.common import cliutils
|
|
from nova.openstack.common import importutils
|
|
from nova.openstack.common import log as logging
|
|
from nova.openstack.common import rpc
|
|
from nova.openstack.common import timeutils
|
|
from nova import quota
|
|
from nova.scheduler import rpcapi as scheduler_rpcapi
|
|
from nova import utils
|
|
from nova import version
|
|
|
|
CONF = cfg.CONF
|
|
CONF.import_opt('network_manager', 'nova.service')
|
|
CONF.import_opt('service_down_time', 'nova.service')
|
|
CONF.import_opt('flat_network_bridge', 'nova.network.manager')
|
|
CONF.import_opt('num_networks', 'nova.network.manager')
|
|
CONF.import_opt('multi_host', 'nova.network.manager')
|
|
CONF.import_opt('network_size', 'nova.network.manager')
|
|
CONF.import_opt('vlan_start', 'nova.network.manager')
|
|
CONF.import_opt('vpn_start', 'nova.network.manager')
|
|
CONF.import_opt('default_floating_pool', 'nova.network.manager')
|
|
CONF.import_opt('public_interface', 'nova.network.linux_net')
|
|
|
|
QUOTAS = quota.QUOTAS
|
|
|
|
|
|
# Decorators for actions
|
|
def args(*args, **kwargs):
|
|
def _decorator(func):
|
|
func.__dict__.setdefault('args', []).insert(0, (args, kwargs))
|
|
return func
|
|
return _decorator
|
|
|
|
|
|
def param2id(object_id):
|
|
"""Helper function to convert various volume id types to internal id.
|
|
args: [object_id], e.g. 'vol-0000000a' or 'volume-0000000a' or '10'
|
|
"""
|
|
if '-' in object_id:
|
|
return ec2utils.ec2_vol_id_to_uuid(object_id)
|
|
else:
|
|
return object_id
|
|
|
|
|
|
class VpnCommands(object):
|
|
"""Class for managing VPNs."""
|
|
|
|
@args('--project', dest="project_id", metavar='<Project name>',
|
|
help='Project name')
|
|
@args('--ip', dest="ip", metavar='<IP Address>', help='IP Address')
|
|
@args('--port', dest="port", metavar='<Port>', help='Port')
|
|
def change(self, project_id, ip, port):
|
|
"""Change the ip and port for a vpn.
|
|
|
|
this will update all networks associated with a project
|
|
not sure if that's the desired behavior or not, patches accepted
|
|
|
|
"""
|
|
# TODO(tr3buchet): perhaps this shouldn't update all networks
|
|
# associated with a project in the future
|
|
admin_context = context.get_admin_context()
|
|
networks = db.project_get_networks(admin_context, project_id)
|
|
for network in networks:
|
|
db.network_update(admin_context,
|
|
network['id'],
|
|
{'vpn_public_address': ip,
|
|
'vpn_public_port': int(port)})
|
|
|
|
|
|
class ShellCommands(object):
|
|
def bpython(self):
|
|
"""Runs a bpython shell.
|
|
|
|
Falls back to Ipython/python shell if unavailable"""
|
|
self.run('bpython')
|
|
|
|
def ipython(self):
|
|
"""Runs an Ipython shell.
|
|
|
|
Falls back to Python shell if unavailable"""
|
|
self.run('ipython')
|
|
|
|
def python(self):
|
|
"""Runs a python shell.
|
|
|
|
Falls back to Python shell if unavailable"""
|
|
self.run('python')
|
|
|
|
@args('--shell', dest="shell", metavar='<bpython|ipython|python >',
|
|
help='Python shell')
|
|
def run(self, shell=None):
|
|
"""Runs a Python interactive interpreter."""
|
|
if not shell:
|
|
shell = 'bpython'
|
|
|
|
if shell == 'bpython':
|
|
try:
|
|
import bpython
|
|
bpython.embed()
|
|
except ImportError:
|
|
shell = 'ipython'
|
|
if shell == 'ipython':
|
|
try:
|
|
import IPython
|
|
# Explicitly pass an empty list as arguments, because
|
|
# otherwise IPython would use sys.argv from this script.
|
|
shell = IPython.Shell.IPShell(argv=[])
|
|
shell.mainloop()
|
|
except ImportError:
|
|
shell = 'python'
|
|
|
|
if shell == 'python':
|
|
import code
|
|
try:
|
|
# Try activating rlcompleter, because it's handy.
|
|
import readline
|
|
except ImportError:
|
|
pass
|
|
else:
|
|
# We don't have to wrap the following import in a 'try',
|
|
# because we already know 'readline' was imported successfully.
|
|
import rlcompleter
|
|
readline.parse_and_bind("tab:complete")
|
|
code.interact()
|
|
|
|
@args('--path', dest='path', metavar='<path>', help='Script path')
|
|
def script(self, path):
|
|
"""Runs the script from the specifed path with flags set properly.
|
|
arguments: path"""
|
|
exec(compile(open(path).read(), path, 'exec'), locals(), globals())
|
|
|
|
|
|
def _db_error(caught_exception):
|
|
print caught_exception
|
|
print _("The above error may show that the database has not "
|
|
"been created.\nPlease create a database using "
|
|
"'nova-manage db sync' before running this command.")
|
|
exit(1)
|
|
|
|
|
|
class ProjectCommands(object):
|
|
"""Class for managing projects."""
|
|
|
|
@args('--project', dest="project_id", metavar='<Project name>',
|
|
help='Project name')
|
|
@args('--key', dest="key", metavar='<key>', help='Key')
|
|
@args('--value', dest="value", metavar='<value>', help='Value')
|
|
def quota(self, project_id, key=None, value=None):
|
|
"""Set or display quotas for project."""
|
|
ctxt = context.get_admin_context()
|
|
project_quota = QUOTAS.get_project_quotas(ctxt, project_id)
|
|
if key and key in project_quota:
|
|
if value.lower() == 'unlimited':
|
|
value = -1
|
|
try:
|
|
db.quota_update(ctxt, project_id, key, value)
|
|
except exception.ProjectQuotaNotFound:
|
|
db.quota_create(ctxt, project_id, key, value)
|
|
else:
|
|
print _('%(key)s is not a valid quota key. Valid options are: '
|
|
'%(options)s.') % {'key': key,
|
|
'options': ', '.join(project_quota)}
|
|
sys.exit(2)
|
|
project_quota = QUOTAS.get_project_quotas(ctxt, project_id)
|
|
for key, value in project_quota.iteritems():
|
|
if value['limit'] < 0 or value['limit'] is None:
|
|
value['limit'] = 'unlimited'
|
|
print '%s: %s' % (key, value['limit'])
|
|
|
|
@args('--project', dest="project_id", metavar='<Project name>',
|
|
help='Project name')
|
|
def scrub(self, project_id):
|
|
"""Deletes data associated with project."""
|
|
admin_context = context.get_admin_context()
|
|
networks = db.project_get_networks(admin_context, project_id)
|
|
for network in networks:
|
|
db.network_disassociate(admin_context, network['id'])
|
|
groups = db.security_group_get_by_project(admin_context, project_id)
|
|
for group in groups:
|
|
db.security_group_destroy(admin_context, group['id'])
|
|
|
|
|
|
AccountCommands = ProjectCommands
|
|
|
|
|
|
class FixedIpCommands(object):
|
|
"""Class for managing fixed ip."""
|
|
|
|
@args('--host', dest="host", metavar='<host>', help='Host')
|
|
def list(self, host=None):
|
|
"""Lists all fixed ips (optionally by host)."""
|
|
ctxt = context.get_admin_context()
|
|
|
|
try:
|
|
if host is None:
|
|
fixed_ips = db.fixed_ip_get_all(ctxt)
|
|
else:
|
|
fixed_ips = db.fixed_ip_get_all_by_instance_host(ctxt, host)
|
|
except exception.NotFound as ex:
|
|
print _("error: %s") % ex
|
|
sys.exit(2)
|
|
|
|
instances = db.instance_get_all(context.get_admin_context())
|
|
instances_by_uuid = {}
|
|
for instance in instances:
|
|
instances_by_uuid[instance['uuid']] = instance
|
|
|
|
print "%-18s\t%-15s\t%-15s\t%s" % (_('network'),
|
|
_('IP address'),
|
|
_('hostname'),
|
|
_('host'))
|
|
|
|
all_networks = {}
|
|
try:
|
|
# use network_get_all to retrieve all existing networks
|
|
# this is to ensure that IPs associated with deleted networks
|
|
# will not throw exceptions.
|
|
for network in db.network_get_all(context.get_admin_context()):
|
|
all_networks[network.id] = network
|
|
except exception.NoNetworksFound:
|
|
# do not have any networks, so even if there are IPs, these
|
|
# IPs should have been deleted ones, so return.
|
|
print _('No fixed IP found.')
|
|
return
|
|
|
|
has_ip = False
|
|
for fixed_ip in fixed_ips:
|
|
hostname = None
|
|
host = None
|
|
network = all_networks.get(fixed_ip['network_id'])
|
|
if network:
|
|
has_ip = True
|
|
if fixed_ip.get('instance_uuid'):
|
|
instance = instances_by_uuid.get(fixed_ip['instance_uuid'])
|
|
if instance:
|
|
hostname = instance['hostname']
|
|
host = instance['host']
|
|
else:
|
|
print _('WARNING: fixed ip %s allocated to missing'
|
|
' instance') % str(fixed_ip['address'])
|
|
print "%-18s\t%-15s\t%-15s\t%s" % (
|
|
network['cidr'],
|
|
fixed_ip['address'],
|
|
hostname, host)
|
|
|
|
if not has_ip:
|
|
print _('No fixed IP found.')
|
|
|
|
@args('--address', dest="address", metavar='<ip address>',
|
|
help='IP address')
|
|
def reserve(self, address):
|
|
"""Mark fixed ip as reserved
|
|
arguments: address"""
|
|
self._set_reserved(address, True)
|
|
|
|
@args('--address', dest="address", metavar='<ip address>',
|
|
help='IP address')
|
|
def unreserve(self, address):
|
|
"""Mark fixed ip as free to use
|
|
arguments: address"""
|
|
self._set_reserved(address, False)
|
|
|
|
def _set_reserved(self, address, reserved):
|
|
ctxt = context.get_admin_context()
|
|
|
|
try:
|
|
fixed_ip = db.fixed_ip_get_by_address(ctxt, address)
|
|
if fixed_ip is None:
|
|
raise exception.NotFound('Could not find address')
|
|
db.fixed_ip_update(ctxt, fixed_ip['address'],
|
|
{'reserved': reserved})
|
|
except exception.NotFound as ex:
|
|
print _("error: %s") % ex
|
|
sys.exit(2)
|
|
|
|
|
|
class FloatingIpCommands(object):
|
|
"""Class for managing floating ip."""
|
|
|
|
@staticmethod
|
|
def address_to_hosts(addresses):
|
|
"""
|
|
Iterate over hosts within an address range.
|
|
|
|
If an explicit range specifier is missing, the parameter is
|
|
interpreted as a specific individual address.
|
|
"""
|
|
try:
|
|
return [netaddr.IPAddress(addresses)]
|
|
except ValueError:
|
|
net = netaddr.IPNetwork(addresses)
|
|
if net.size < 4:
|
|
reason = _("/%s should be specified as single address(es) "
|
|
"not in cidr format") % net.prefixlen
|
|
raise exception.InvalidInput(reason=reason)
|
|
else:
|
|
return net.iter_hosts()
|
|
|
|
@args('--ip_range', dest="ip_range", metavar='<range>', help='IP range')
|
|
@args('--pool', dest="pool", metavar='<pool>', help='Optional pool')
|
|
@args('--interface', dest="interface", metavar='<interface>',
|
|
help='Optional interface')
|
|
def create(self, ip_range, pool=None, interface=None):
|
|
"""Creates floating ips for zone by range."""
|
|
admin_context = context.get_admin_context()
|
|
if not pool:
|
|
pool = CONF.default_floating_pool
|
|
if not interface:
|
|
interface = CONF.public_interface
|
|
|
|
ips = ({'address': str(address), 'pool': pool, 'interface': interface}
|
|
for address in self.address_to_hosts(ip_range))
|
|
try:
|
|
db.floating_ip_bulk_create(admin_context, ips)
|
|
except exception.FloatingIpExists as exc:
|
|
# NOTE(simplylizz): Maybe logging would be better here
|
|
# instead of printing, but logging isn't used here and I
|
|
# don't know why.
|
|
print('error: %s' % exc)
|
|
sys.exit(1)
|
|
|
|
@args('--ip_range', dest="ip_range", metavar='<range>', help='IP range')
|
|
def delete(self, ip_range):
|
|
"""Deletes floating ips by range."""
|
|
admin_context = context.get_admin_context()
|
|
|
|
ips = ({'address': str(address)}
|
|
for address in self.address_to_hosts(ip_range))
|
|
db.floating_ip_bulk_destroy(admin_context, ips)
|
|
|
|
@args('--host', dest="host", metavar='<host>', help='Host')
|
|
def list(self, host=None):
|
|
"""Lists all floating ips (optionally by host)
|
|
Note: if host is given, only active floating IPs are returned"""
|
|
ctxt = context.get_admin_context()
|
|
try:
|
|
if host is None:
|
|
floating_ips = db.floating_ip_get_all(ctxt)
|
|
else:
|
|
floating_ips = db.floating_ip_get_all_by_host(ctxt, host)
|
|
except exception.NoFloatingIpsDefined:
|
|
print _("No floating IP addresses have been defined.")
|
|
return
|
|
for floating_ip in floating_ips:
|
|
instance_uuid = None
|
|
if floating_ip['fixed_ip_id']:
|
|
fixed_ip = db.fixed_ip_get(ctxt, floating_ip['fixed_ip_id'])
|
|
instance_uuid = fixed_ip['instance_uuid']
|
|
|
|
print "%s\t%s\t%s\t%s\t%s" % (floating_ip['project_id'],
|
|
floating_ip['address'],
|
|
instance_uuid,
|
|
floating_ip['pool'],
|
|
floating_ip['interface'])
|
|
|
|
|
|
class NetworkCommands(object):
|
|
"""Class for managing networks."""
|
|
|
|
@args('--label', dest="label", metavar='<label>',
|
|
help='Label for network (ex: public)')
|
|
@args('--fixed_range_v4', dest="cidr", metavar='<x.x.x.x/yy>',
|
|
help='IPv4 subnet (ex: 10.0.0.0/8)')
|
|
@args('--num_networks', dest="num_networks", metavar='<number>',
|
|
help='Number of networks to create')
|
|
@args('--network_size', dest="network_size", metavar='<number>',
|
|
help='Number of IPs per network')
|
|
@args('--vlan', dest="vlan_start", metavar='<vlan id>', help='vlan id')
|
|
@args('--vpn', dest="vpn_start", help='vpn start')
|
|
@args('--fixed_range_v6', dest="cidr_v6",
|
|
help='IPv6 subnet (ex: fe80::/64')
|
|
@args('--gateway', dest="gateway", help='gateway')
|
|
@args('--gateway_v6', dest="gateway_v6", help='ipv6 gateway')
|
|
@args('--bridge', dest="bridge",
|
|
metavar='<bridge>',
|
|
help='VIFs on this network are connected to this bridge')
|
|
@args('--bridge_interface', dest="bridge_interface",
|
|
metavar='<bridge interface>',
|
|
help='the bridge is connected to this interface')
|
|
@args('--multi_host', dest="multi_host", metavar="<'T'|'F'>",
|
|
help='Multi host')
|
|
@args('--dns1', dest="dns1", metavar="<DNS Address>", help='First DNS')
|
|
@args('--dns2', dest="dns2", metavar="<DNS Address>", help='Second DNS')
|
|
@args('--uuid', dest="uuid", metavar="<network uuid>",
|
|
help='Network UUID')
|
|
@args('--fixed_cidr', dest="fixed_cidr", metavar='<x.x.x.x/yy>',
|
|
help='IPv4 subnet for fixed IPS (ex: 10.20.0.0/16)')
|
|
@args('--project_id', dest="project_id", metavar="<project id>",
|
|
help='Project id')
|
|
@args('--priority', dest="priority", metavar="<number>",
|
|
help='Network interface priority')
|
|
def create(self, label=None, cidr=None, num_networks=None,
|
|
network_size=None, multi_host=None, vlan_start=None,
|
|
vpn_start=None, cidr_v6=None, gateway=None,
|
|
gateway_v6=None, bridge=None, bridge_interface=None,
|
|
dns1=None, dns2=None, project_id=None, priority=None,
|
|
uuid=None, fixed_cidr=None):
|
|
"""Creates fixed ips for host by range."""
|
|
kwargs = dict(((k, v) for k, v in locals().iteritems()
|
|
if v and k != "self"))
|
|
if multi_host is not None:
|
|
kwargs['multi_host'] = multi_host == 'T'
|
|
net_manager = importutils.import_object(CONF.network_manager)
|
|
net_manager.create_networks(context.get_admin_context(), **kwargs)
|
|
|
|
def list(self):
|
|
"""List all created networks."""
|
|
_fmt = "%-5s\t%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s"
|
|
print _fmt % (_('id'),
|
|
_('IPv4'),
|
|
_('IPv6'),
|
|
_('start address'),
|
|
_('DNS1'),
|
|
_('DNS2'),
|
|
_('VlanID'),
|
|
_('project'),
|
|
_("uuid"))
|
|
try:
|
|
# Since network_get_all can throw exception.NoNetworksFound
|
|
# for this command to show a nice result, this exception
|
|
# should be caught and handled as such.
|
|
networks = db.network_get_all(context.get_admin_context())
|
|
except exception.NoNetworksFound:
|
|
print _('No networks found')
|
|
else:
|
|
for network in networks:
|
|
print _fmt % (network.id,
|
|
network.cidr,
|
|
network.cidr_v6,
|
|
network.dhcp_start,
|
|
network.dns1,
|
|
network.dns2,
|
|
network.vlan,
|
|
network.project_id,
|
|
network.uuid)
|
|
|
|
@args('--fixed_range', dest="fixed_range", metavar='<x.x.x.x/yy>',
|
|
help='Network to delete')
|
|
@args('--uuid', dest='uuid', metavar='<uuid>',
|
|
help='UUID of network to delete')
|
|
def delete(self, fixed_range=None, uuid=None):
|
|
"""Deletes a network."""
|
|
|
|
if fixed_range is None and uuid is None:
|
|
raise Exception(_("Please specify either fixed_range or uuid"))
|
|
|
|
net_manager = importutils.import_object(CONF.network_manager)
|
|
if "QuantumManager" in CONF.network_manager:
|
|
if uuid is None:
|
|
raise Exception(_("UUID is required to delete "
|
|
"Quantum Networks"))
|
|
if fixed_range:
|
|
raise Exception(_("Deleting by fixed_range is not supported "
|
|
"with the QuantumManager"))
|
|
# delete the network
|
|
net_manager.delete_network(context.get_admin_context(),
|
|
fixed_range, uuid)
|
|
|
|
@args('--fixed_range', dest="fixed_range", metavar='<x.x.x.x/yy>',
|
|
help='Network to modify')
|
|
@args('--project', dest="project", metavar='<project name>',
|
|
help='Project name to associate')
|
|
@args('--host', dest="host", metavar='<host>',
|
|
help='Host to associate')
|
|
@args('--disassociate-project', action="store_true", dest='dis_project',
|
|
default=False, help='Disassociate Network from Project')
|
|
@args('--disassociate-host', action="store_true", dest='dis_host',
|
|
default=False, help='Disassociate Host from Project')
|
|
def modify(self, fixed_range, project=None, host=None,
|
|
dis_project=None, dis_host=None):
|
|
"""Associate/Disassociate Network with Project and/or Host
|
|
arguments: network project host
|
|
leave any field blank to ignore it
|
|
"""
|
|
admin_context = context.get_admin_context()
|
|
network = db.network_get_by_cidr(admin_context, fixed_range)
|
|
net = {}
|
|
#User can choose the following actions each for project and host.
|
|
#1) Associate (set not None value given by project/host parameter)
|
|
#2) Disassociate (set None by disassociate parameter)
|
|
#3) Keep unchanged (project/host key is not added to 'net')
|
|
if project:
|
|
net['project_id'] = project
|
|
elif dis_project:
|
|
net['project_id'] = None
|
|
if host:
|
|
net['host'] = host
|
|
elif dis_host:
|
|
net['host'] = None
|
|
db.network_update(admin_context, network['id'], net)
|
|
|
|
|
|
class VmCommands(object):
|
|
"""Class for mangaging VM instances."""
|
|
|
|
@args('--host', dest="host", metavar='<host>', help='Host')
|
|
def list(self, host=None):
|
|
"""Show a list of all instances."""
|
|
|
|
print ("%-10s %-15s %-10s %-10s %-26s %-9s %-9s %-9s"
|
|
" %-10s %-10s %-10s %-5s" % (_('instance'),
|
|
_('node'),
|
|
_('type'),
|
|
_('state'),
|
|
_('launched'),
|
|
_('image'),
|
|
_('kernel'),
|
|
_('ramdisk'),
|
|
_('project'),
|
|
_('user'),
|
|
_('zone'),
|
|
_('index')))
|
|
|
|
if host is None:
|
|
instances = db.instance_get_all(context.get_admin_context())
|
|
else:
|
|
instances = db.instance_get_all_by_host(
|
|
context.get_admin_context(), host)
|
|
|
|
for instance in instances:
|
|
print ("%-10s %-15s %-10s %-10s %-26s %-9s %-9s %-9s"
|
|
" %-10s %-10s %-10s %-5d" % (instance['display_name'],
|
|
instance['host'],
|
|
instance['instance_type'].name,
|
|
instance['vm_state'],
|
|
instance['launched_at'],
|
|
instance['image_ref'],
|
|
instance['kernel_id'],
|
|
instance['ramdisk_id'],
|
|
instance['project_id'],
|
|
instance['user_id'],
|
|
instance['availability_zone'],
|
|
instance['launch_index']))
|
|
|
|
|
|
class ServiceCommands(object):
|
|
"""Enable and disable running services."""
|
|
|
|
@args('--host', dest='host', metavar='<host>', help='Host')
|
|
@args('--service', dest='service', metavar='<service>',
|
|
help='Nova service')
|
|
def list(self, host=None, service=None):
|
|
"""
|
|
Show a list of all running services. Filter by host & service name.
|
|
"""
|
|
ctxt = context.get_admin_context()
|
|
now = timeutils.utcnow()
|
|
services = db.service_get_all(ctxt)
|
|
services = availability_zones.set_availability_zones(ctxt, services)
|
|
if host:
|
|
services = [s for s in services if s['host'] == host]
|
|
if service:
|
|
services = [s for s in services if s['binary'] == service]
|
|
print_format = "%-16s %-36s %-16s %-10s %-5s %-10s"
|
|
print print_format % (
|
|
_('Binary'),
|
|
_('Host'),
|
|
_('Zone'),
|
|
_('Status'),
|
|
_('State'),
|
|
_('Updated_At'))
|
|
for svc in services:
|
|
delta = now - (svc['updated_at'] or svc['created_at'])
|
|
alive = abs(utils.total_seconds(delta)) <= CONF.service_down_time
|
|
art = (alive and ":-)") or "XXX"
|
|
active = 'enabled'
|
|
if svc['disabled']:
|
|
active = 'disabled'
|
|
print print_format % (svc['binary'], svc['host'],
|
|
svc['availability_zone'], active, art,
|
|
svc['updated_at'])
|
|
|
|
@args('--host', dest='host', metavar='<host>', help='Host')
|
|
@args('--service', dest='service', metavar='<service>',
|
|
help='Nova service')
|
|
def enable(self, host, service):
|
|
"""Enable scheduling for a service."""
|
|
ctxt = context.get_admin_context()
|
|
svc = db.service_get_by_args(ctxt, host, service)
|
|
if not svc:
|
|
print _("Unable to find service")
|
|
return
|
|
db.service_update(ctxt, svc['id'], {'disabled': False})
|
|
|
|
@args('--host', dest='host', metavar='<host>', help='Host')
|
|
@args('--service', dest='service', metavar='<service>',
|
|
help='Nova service')
|
|
def disable(self, host, service):
|
|
"""Disable scheduling for a service."""
|
|
ctxt = context.get_admin_context()
|
|
svc = db.service_get_by_args(ctxt, host, service)
|
|
if not svc:
|
|
print _("Unable to find service")
|
|
return
|
|
db.service_update(ctxt, svc['id'], {'disabled': True})
|
|
|
|
@args('--host', dest='host', metavar='<host>', help='Host')
|
|
def describe_resource(self, host):
|
|
"""Describes cpu/memory/hdd info for host.
|
|
|
|
:param host: hostname.
|
|
|
|
"""
|
|
rpcapi = scheduler_rpcapi.SchedulerAPI()
|
|
result = rpcapi.show_host_resources(context.get_admin_context(),
|
|
host=host)
|
|
|
|
if not isinstance(result, dict):
|
|
print _('An unexpected error has occurred.')
|
|
print _('[Result]'), result
|
|
else:
|
|
# Printing a total and used_now
|
|
# (NOTE)The host name width 16 characters
|
|
print '%(a)-25s%(b)16s%(c)8s%(d)8s%(e)8s' % {"a": _('HOST'),
|
|
"b": _('PROJECT'),
|
|
"c": _('cpu'),
|
|
"d": _('mem(mb)'),
|
|
"e": _('hdd')}
|
|
print ('%(a)-16s(total)%(b)26s%(c)8s%(d)8s' %
|
|
{"a": host,
|
|
"b": result['resource']['vcpus'],
|
|
"c": result['resource']['memory_mb'],
|
|
"d": result['resource']['local_gb']})
|
|
|
|
print ('%(a)-16s(used_now)%(b)23s%(c)8s%(d)8s' %
|
|
{"a": host,
|
|
"b": result['resource']['vcpus_used'],
|
|
"c": result['resource']['memory_mb_used'],
|
|
"d": result['resource']['local_gb_used']})
|
|
|
|
# Printing a used_max
|
|
cpu_sum = 0
|
|
mem_sum = 0
|
|
hdd_sum = 0
|
|
for p_id, val in result['usage'].items():
|
|
cpu_sum += val['vcpus']
|
|
mem_sum += val['memory_mb']
|
|
hdd_sum += val['root_gb']
|
|
hdd_sum += val['ephemeral_gb']
|
|
print '%(a)-16s(used_max)%(b)23s%(c)8s%(d)8s' % {"a": host,
|
|
"b": cpu_sum,
|
|
"c": mem_sum,
|
|
"d": hdd_sum}
|
|
|
|
for p_id, val in result['usage'].items():
|
|
print '%(a)-25s%(b)16s%(c)8s%(d)8s%(e)8s' % {
|
|
"a": host,
|
|
"b": p_id,
|
|
"c": val['vcpus'],
|
|
"d": val['memory_mb'],
|
|
"e": val['root_gb'] + val['ephemeral_gb']}
|
|
|
|
|
|
class HostCommands(object):
|
|
"""List hosts."""
|
|
|
|
def list(self, zone=None):
|
|
"""Show a list of all physical hosts. Filter by zone.
|
|
args: [zone]"""
|
|
print "%-25s\t%-15s" % (_('host'),
|
|
_('zone'))
|
|
ctxt = context.get_admin_context()
|
|
services = db.service_get_all(ctxt)
|
|
services = availability_zones.set_availability_zones(ctxt, services)
|
|
if zone:
|
|
services = [s for s in services if s['availability_zone'] == zone]
|
|
hosts = []
|
|
for srv in services:
|
|
if not [h for h in hosts if h['host'] == srv['host']]:
|
|
hosts.append(srv)
|
|
|
|
for h in hosts:
|
|
print "%-25s\t%-15s" % (h['host'], h['availability_zone'])
|
|
|
|
|
|
class DbCommands(object):
|
|
"""Class for managing the database."""
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
@args('--version', dest='version', metavar='<version>',
|
|
help='Database version')
|
|
def sync(self, version=None):
|
|
"""Sync the database up to the most recent version."""
|
|
return migration.db_sync(version)
|
|
|
|
def version(self):
|
|
"""Print the current database version."""
|
|
print migration.db_version()
|
|
|
|
|
|
class InstanceTypeCommands(object):
|
|
"""Class for managing instance types / flavors."""
|
|
|
|
def _print_instance_types(self, name, val):
|
|
is_public = ('private', 'public')[val["is_public"] == 1]
|
|
print ("%s: Memory: %sMB, VCPUS: %s, Root: %sGB, Ephemeral: %sGb, "
|
|
"FlavorID: %s, Swap: %sMB, RXTX Factor: %s, %s, ExtraSpecs %s") % (
|
|
name, val["memory_mb"], val["vcpus"], val["root_gb"],
|
|
val["ephemeral_gb"], val["flavorid"], val["swap"],
|
|
val["rxtx_factor"], is_public, val["extra_specs"])
|
|
|
|
@args('--name', dest='name', metavar='<name>',
|
|
help='Name of instance type/flavor')
|
|
@args('--memory', dest='memory', metavar='<memory size>',
|
|
help='Memory size')
|
|
@args('--cpu', dest='vcpus', metavar='<num cores>', help='Number cpus')
|
|
@args('--root_gb', dest='root_gb', metavar='<root_gb>',
|
|
help='Root disk size')
|
|
@args('--ephemeral_gb', dest='ephemeral_gb', metavar='<ephemeral_gb>',
|
|
help='Ephemeral disk size')
|
|
@args('--flavor', dest='flavorid', metavar='<flavor id>',
|
|
help='Flavor ID')
|
|
@args('--swap', dest='swap', metavar='<swap>', help='Swap')
|
|
@args('--rxtx_factor', dest='rxtx_factor', metavar='<rxtx_factor>',
|
|
help='rxtx_factor')
|
|
@args('--is_public', dest="is_public", metavar='<is_public>',
|
|
help='Make flavor accessible to the public')
|
|
def create(self, name, memory, vcpus, root_gb, ephemeral_gb=0,
|
|
flavorid=None, swap=0, rxtx_factor=1.0, is_public=True):
|
|
"""Creates instance types / flavors."""
|
|
try:
|
|
instance_types.create(name, memory, vcpus, root_gb,
|
|
ephemeral_gb, flavorid, swap, rxtx_factor,
|
|
is_public)
|
|
except exception.InvalidInput, e:
|
|
print _("Must supply valid parameters to create instance_type")
|
|
print e
|
|
sys.exit(1)
|
|
except exception.InstanceTypeExists:
|
|
print _("Instance Type exists.")
|
|
print _("Please ensure instance_type name and flavorid are "
|
|
"unique.")
|
|
print _("Currently defined instance_type names and flavorids:")
|
|
print
|
|
self.list()
|
|
sys.exit(2)
|
|
except Exception:
|
|
print _("Unknown error")
|
|
sys.exit(3)
|
|
else:
|
|
print _("%s created") % name
|
|
|
|
@args('--name', dest='name', metavar='<name>',
|
|
help='Name of instance type/flavor')
|
|
def delete(self, name):
|
|
"""Marks instance types / flavors as deleted."""
|
|
try:
|
|
instance_types.destroy(name)
|
|
except exception.InstanceTypeNotFound:
|
|
print _("Valid instance type name is required")
|
|
sys.exit(1)
|
|
except exception.DBError, e:
|
|
print _("DB Error: %s") % e
|
|
sys.exit(2)
|
|
except Exception:
|
|
sys.exit(3)
|
|
else:
|
|
print _("%s deleted") % name
|
|
|
|
@args('--name', dest='name', metavar='<name>',
|
|
help='Name of instance type/flavor')
|
|
def list(self, name=None):
|
|
"""Lists all active or specific instance types / flavors."""
|
|
try:
|
|
if name is None:
|
|
inst_types = instance_types.get_all_types()
|
|
else:
|
|
inst_types = instance_types.get_instance_type_by_name(name)
|
|
except exception.DBError, e:
|
|
_db_error(e)
|
|
if isinstance(inst_types.values()[0], dict):
|
|
for k, v in inst_types.iteritems():
|
|
self._print_instance_types(k, v)
|
|
else:
|
|
self._print_instance_types(name, inst_types)
|
|
|
|
@args('--name', dest='name', metavar='<name>',
|
|
help='Name of instance type/flavor')
|
|
@args('--key', dest='key', metavar='<key>',
|
|
help='The key of the key/value pair')
|
|
@args('--value', dest='value', metavar='<value>',
|
|
help='The value of the key/value pair')
|
|
def set_key(self, name, key, value=None):
|
|
"""Add key/value pair to specified instance type's extra_specs."""
|
|
try:
|
|
try:
|
|
inst_type = instance_types.get_instance_type_by_name(name)
|
|
except exception.InstanceTypeNotFoundByName, e:
|
|
print e
|
|
sys.exit(2)
|
|
|
|
ctxt = context.get_admin_context()
|
|
ext_spec = {key: value}
|
|
db.instance_type_extra_specs_update_or_create(
|
|
ctxt,
|
|
inst_type["flavorid"],
|
|
ext_spec)
|
|
print _("Key %(key)s set to %(value)s on instance"
|
|
" type %(name)s") % locals()
|
|
except exception.DBError, e:
|
|
_db_error(e)
|
|
|
|
@args('--name', dest='name', metavar='<name>',
|
|
help='Name of instance type/flavor')
|
|
@args('--key', dest='key', metavar='<key>',
|
|
help='The key to be deleted')
|
|
def unset_key(self, name, key):
|
|
"""Delete the specified extra spec for instance type."""
|
|
try:
|
|
try:
|
|
inst_type = instance_types.get_instance_type_by_name(name)
|
|
except exception.InstanceTypeNotFoundByName, e:
|
|
print e
|
|
sys.exit(2)
|
|
|
|
ctxt = context.get_admin_context()
|
|
db.instance_type_extra_specs_delete(
|
|
ctxt,
|
|
inst_type["flavorid"],
|
|
key)
|
|
|
|
print _("Key %(key)s on instance type %(name)s unset") % locals()
|
|
except exception.DBError, e:
|
|
_db_error(e)
|
|
|
|
|
|
class AgentBuildCommands(object):
|
|
"""Class for managing agent builds."""
|
|
|
|
def create(self, os, architecture, version, url, md5hash,
|
|
hypervisor='xen'):
|
|
"""Creates a new agent build."""
|
|
ctxt = context.get_admin_context()
|
|
agent_build = db.agent_build_create(ctxt,
|
|
{'hypervisor': hypervisor,
|
|
'os': os,
|
|
'architecture': architecture,
|
|
'version': version,
|
|
'url': url,
|
|
'md5hash': md5hash})
|
|
|
|
def delete(self, os, architecture, hypervisor='xen'):
|
|
"""Deletes an existing agent build."""
|
|
ctxt = context.get_admin_context()
|
|
agent_build_ref = db.agent_build_get_by_triple(ctxt,
|
|
hypervisor, os, architecture)
|
|
db.agent_build_destroy(ctxt, agent_build_ref['id'])
|
|
|
|
def list(self, hypervisor=None):
|
|
"""Lists all agent builds.
|
|
arguments: <none>"""
|
|
fmt = "%-10s %-8s %12s %s"
|
|
ctxt = context.get_admin_context()
|
|
by_hypervisor = {}
|
|
for agent_build in db.agent_build_get_all(ctxt):
|
|
buildlist = by_hypervisor.get(agent_build.hypervisor)
|
|
if not buildlist:
|
|
buildlist = by_hypervisor[agent_build.hypervisor] = []
|
|
|
|
buildlist.append(agent_build)
|
|
|
|
for key, buildlist in by_hypervisor.iteritems():
|
|
if hypervisor and key != hypervisor:
|
|
continue
|
|
|
|
print _('Hypervisor: %s') % key
|
|
print fmt % ('-' * 10, '-' * 8, '-' * 12, '-' * 32)
|
|
for agent_build in buildlist:
|
|
print fmt % (agent_build.os, agent_build.architecture,
|
|
agent_build.version, agent_build.md5hash)
|
|
print ' %s' % agent_build.url
|
|
|
|
print
|
|
|
|
def modify(self, os, architecture, version, url, md5hash,
|
|
hypervisor='xen'):
|
|
"""Update an existing agent build."""
|
|
ctxt = context.get_admin_context()
|
|
agent_build_ref = db.agent_build_get_by_triple(ctxt,
|
|
hypervisor, os, architecture)
|
|
db.agent_build_update(ctxt, agent_build_ref['id'],
|
|
{'version': version,
|
|
'url': url,
|
|
'md5hash': md5hash})
|
|
|
|
|
|
class GetLogCommands(object):
|
|
"""Get logging information."""
|
|
|
|
def errors(self):
|
|
"""Get all of the errors from the log files."""
|
|
error_found = 0
|
|
if CONF.log_dir:
|
|
logs = [x for x in os.listdir(CONF.log_dir) if x.endswith('.log')]
|
|
for file in logs:
|
|
log_file = os.path.join(CONF.log_dir, file)
|
|
lines = [line.strip() for line in open(log_file, "r")]
|
|
lines.reverse()
|
|
print_name = 0
|
|
for index, line in enumerate(lines):
|
|
if line.find(" ERROR ") > 0:
|
|
error_found += 1
|
|
if print_name == 0:
|
|
print log_file + ":-"
|
|
print_name = 1
|
|
linenum = len(lines) - index
|
|
print _('Line %(linenum)d : %(line)s') % locals()
|
|
if error_found == 0:
|
|
print _('No errors in logfiles!')
|
|
|
|
def syslog(self, num_entries=10):
|
|
"""Get <num_entries> of the nova syslog events."""
|
|
entries = int(num_entries)
|
|
count = 0
|
|
log_file = ''
|
|
if os.path.exists('/var/log/syslog'):
|
|
log_file = '/var/log/syslog'
|
|
elif os.path.exists('/var/log/messages'):
|
|
log_file = '/var/log/messages'
|
|
else:
|
|
print _('Unable to find system log file!')
|
|
sys.exit(1)
|
|
lines = [line.strip() for line in open(log_file, "r")]
|
|
lines.reverse()
|
|
print _('Last %s nova syslog entries:-') % (entries)
|
|
for line in lines:
|
|
if line.find("nova") > 0:
|
|
count += 1
|
|
print "%s" % (line)
|
|
if count == entries:
|
|
break
|
|
|
|
if count == 0:
|
|
print _('No nova entries in syslog!')
|
|
|
|
|
|
class CellCommands(object):
|
|
"""Commands for managing cells."""
|
|
|
|
@args('--name', dest='name', metavar='<name>',
|
|
help='Name for the new cell')
|
|
@args('--cell_type', dest='cell_type', metavar='<parent|child>',
|
|
help='Whether the cell is a parent or child')
|
|
@args('--username', dest='username', metavar='<username>',
|
|
help='Username for the message broker in this cell')
|
|
@args('--password', dest='password', metavar='<password>',
|
|
help='Password for the message broker in this cell')
|
|
@args('--hostname', dest='hostname', metavar='<hostname>',
|
|
help='Address of the message broker in this cell')
|
|
@args('--port', dest='port', metavar='<number>',
|
|
help='Port number of the message broker in this cell')
|
|
@args('--virtual_host', dest='virtual_host', metavar='<virtual_host>',
|
|
help='The virtual host of the message broker in this cell')
|
|
@args('--woffset', dest='woffset', metavar='<float>')
|
|
@args('--wscale', dest='wscale', metavar='<float>')
|
|
def create(self, name, cell_type='child', username=None, password=None,
|
|
hostname=None, port=None, virtual_host=None,
|
|
woffset=None, wscale=None):
|
|
|
|
if cell_type not in ['parent', 'child']:
|
|
print "Error: cell type must be 'parent' or 'child'"
|
|
sys.exit(2)
|
|
|
|
is_parent = cell_type == 'parent'
|
|
values = {'name': name,
|
|
'is_parent': is_parent,
|
|
'username': username,
|
|
'password': password,
|
|
'rpc_host': hostname,
|
|
'rpc_port': int(port),
|
|
'rpc_virtual_host': virtual_host,
|
|
'weight_offset': float(woffset),
|
|
'weight_scale': float(wscale)}
|
|
ctxt = context.get_admin_context()
|
|
db.cell_create(ctxt, values)
|
|
|
|
@args('--cell_id', dest='cell_id', metavar='<cell_id>',
|
|
help='ID of the cell to delete')
|
|
def delete(self, cell_id):
|
|
ctxt = context.get_admin_context()
|
|
db.cell_delete(ctxt, cell_id)
|
|
|
|
def list(self):
|
|
ctxt = context.get_admin_context()
|
|
cells = db.cell_get_all(ctxt)
|
|
fmt = "%3s %-10s %-6s %-10s %-15s %-5s %-10s"
|
|
print fmt % ('Id', 'Name', 'Type', 'Username', 'Hostname',
|
|
'Port', 'VHost')
|
|
print fmt % ('-' * 3, '-' * 10, '-' * 6, '-' * 10, '-' * 15,
|
|
'-' * 5, '-' * 10)
|
|
for cell in cells:
|
|
print fmt % (cell.id, cell.name,
|
|
'parent' if cell.is_parent else 'child',
|
|
cell.username, cell.rpc_host,
|
|
cell.rpc_port, cell.rpc_virtual_host)
|
|
print fmt % ('-' * 3, '-' * 10, '-' * 6, '-' * 10, '-' * 15,
|
|
'-' * 5, '-' * 10)
|
|
|
|
|
|
CATEGORIES = {
|
|
'account': AccountCommands,
|
|
'agent': AgentBuildCommands,
|
|
'cell': CellCommands,
|
|
'db': DbCommands,
|
|
'fixed': FixedIpCommands,
|
|
'flavor': InstanceTypeCommands,
|
|
'floating': FloatingIpCommands,
|
|
'host': HostCommands,
|
|
'instance_type': InstanceTypeCommands,
|
|
'logs': GetLogCommands,
|
|
'network': NetworkCommands,
|
|
'project': ProjectCommands,
|
|
'service': ServiceCommands,
|
|
'shell': ShellCommands,
|
|
'vm': VmCommands,
|
|
'vpn': VpnCommands,
|
|
}
|
|
|
|
|
|
def methods_of(obj):
|
|
"""Get all callable methods of an object that don't start with underscore
|
|
returns a list of tuples of the form (method_name, method)"""
|
|
result = []
|
|
for i in dir(obj):
|
|
if callable(getattr(obj, i)) and not i.startswith('_'):
|
|
result.append((i, getattr(obj, i)))
|
|
return result
|
|
|
|
|
|
def add_command_parsers(subparsers):
|
|
parser = subparsers.add_parser('version')
|
|
|
|
parser = subparsers.add_parser('bash-completion')
|
|
parser.add_argument('query_category', nargs='?')
|
|
|
|
for category in CATEGORIES:
|
|
command_object = CATEGORIES[category]()
|
|
|
|
parser = subparsers.add_parser(category)
|
|
parser.set_defaults(command_object=command_object)
|
|
|
|
category_subparsers = parser.add_subparsers(dest='action')
|
|
|
|
for (action, action_fn) in methods_of(command_object):
|
|
parser = category_subparsers.add_parser(action)
|
|
|
|
action_kwargs = []
|
|
for args, kwargs in getattr(action_fn, 'args', []):
|
|
action_kwargs.append(kwargs['dest'])
|
|
kwargs['dest'] = 'action_kwarg_' + kwargs['dest']
|
|
parser.add_argument(*args, **kwargs)
|
|
|
|
parser.set_defaults(action_fn=action_fn)
|
|
parser.set_defaults(action_kwargs=action_kwargs)
|
|
|
|
parser.add_argument('action_args', nargs='*')
|
|
|
|
|
|
category_opt = cfg.SubCommandOpt('category',
|
|
title='Command categories',
|
|
help='Available categories',
|
|
handler=add_command_parsers)
|
|
|
|
|
|
def main():
|
|
"""Parse options and call the appropriate class/method."""
|
|
CONF.register_cli_opt(category_opt)
|
|
try:
|
|
config.parse_args(sys.argv)
|
|
logging.setup("nova")
|
|
except cfg.ConfigFilesNotFoundError:
|
|
cfgfile = CONF.config_file[-1] if CONF.config_file else None
|
|
if cfgfile and not os.access(cfgfile, os.R_OK):
|
|
st = os.stat(cfgfile)
|
|
print _("Could not read %s. Re-running with sudo") % cfgfile
|
|
try:
|
|
os.execvp('sudo', ['sudo', '-u', '#%s' % st.st_uid] + sys.argv)
|
|
except Exception:
|
|
print _('sudo failed, continuing as if nothing happened')
|
|
|
|
print _('Please re-run nova-manage as root.')
|
|
sys.exit(2)
|
|
|
|
if CONF.category.name == "version":
|
|
print version.version_string_with_package()
|
|
sys.exit(0)
|
|
|
|
if CONF.category.name == "bash-completion":
|
|
if not CONF.category.query_category:
|
|
print " ".join(CATEGORIES.keys())
|
|
elif CONF.category.query_category in CATEGORIES:
|
|
fn = CATEGORIES[CONF.category.query_category]
|
|
command_object = fn()
|
|
actions = methods_of(command_object)
|
|
print " ".join([k for (k, v) in actions])
|
|
sys.exit(0)
|
|
|
|
fn = CONF.category.action_fn
|
|
fn_args = [arg.decode('utf-8') for arg in CONF.category.action_args]
|
|
fn_kwargs = {}
|
|
for k in CONF.category.action_kwargs:
|
|
v = getattr(CONF.category, 'action_kwarg_' + k)
|
|
if v is None:
|
|
continue
|
|
if isinstance(v, basestring):
|
|
v = v.decode('utf-8')
|
|
fn_kwargs[k] = v
|
|
|
|
# call the action with the remaining arguments
|
|
# check arguments
|
|
try:
|
|
cliutils.validate_args(fn, *fn_args, **fn_kwargs)
|
|
except cliutils.MissingArgs as e:
|
|
# NOTE(mikal): this isn't the most helpful error message ever. It is
|
|
# long, and tells you a lot of things you probably don't want to know
|
|
# if you just got a single arg wrong.
|
|
print fn.__doc__
|
|
CONF.print_help()
|
|
print e
|
|
sys.exit(1)
|
|
try:
|
|
fn(*fn_args, **fn_kwargs)
|
|
rpc.cleanup()
|
|
sys.exit(0)
|
|
except Exception:
|
|
print _("Command failed, please check log for more info")
|
|
raise
|
|
|
|
if __name__ == '__main__':
|
|
main()
|