# 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. # Copyright 2013 Red Hat, 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. # 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. """ from __future__ import print_function import argparse import functools import os import re import sys import traceback import decorator import netaddr from oslo_config import cfg from oslo_db import exception as db_exc from oslo_log import log as logging import oslo_messaging as messaging from oslo_utils import importutils from oslo_utils import uuidutils import prettytable import six import six.moves.urllib.parse as urlparse from sqlalchemy.engine import url as sqla_url from nova.api.ec2 import ec2utils from nova import availability_zones from nova.cmd import common as cmd_common import nova.conf from nova import config from nova import context from nova import db from nova.db import migration from nova import exception from nova.i18n import _ from nova import objects from nova.objects import aggregate as aggregate_obj from nova.objects import build_request as build_request_obj from nova.objects import flavor as flavor_obj from nova.objects import host_mapping as host_mapping_obj from nova.objects import instance as instance_obj from nova.objects import instance_group as instance_group_obj from nova.objects import keypair as keypair_obj from nova.objects import quotas as quotas_obj from nova.objects import request_spec from nova import quota from nova import rpc from nova import utils from nova import version from nova.virt import ironic CONF = nova.conf.CONF QUOTAS = quota.QUOTAS # Keep this list sorted and one entry per line for readability. _EXTRA_DEFAULT_LOG_LEVELS = ['oslo_concurrency=INFO', 'oslo_db=INFO', 'oslo_policy=INFO'] # Decorators for actions args = cmd_common.args action_description = cmd_common.action_description 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 def mask_passwd_in_url(url): parsed = urlparse.urlparse(url) safe_netloc = re.sub(':.*@', ':****@', parsed.netloc) new_parsed = urlparse.ParseResult( parsed.scheme, safe_netloc, parsed.path, parsed.params, parsed.query, parsed.fragment) return urlparse.urlunparse(new_parsed) class ShellCommands(object): # TODO(stephenfin): Remove this during the Queens cycle description = ('DEPRECATED: The shell commands are deprecated since ' 'Pike as they serve no useful purpose in modern nova. ' 'They will be removed in an upcoming release.') 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', metavar='', 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: from IPython import embed embed() except ImportError: try: # Ipython < 0.11 # Explicitly pass an empty list as arguments, because # otherwise IPython would use sys.argv from this script. import IPython shell = IPython.Shell.IPShell(argv=[]) shell.mainloop() except ImportError: # no IPython module 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. readline.parse_and_bind("tab:complete") code.interact() @args('--path', metavar='', help='Script path') def script(self, path): """Runs the script from the specified 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.")) sys.exit(1) class QuotaCommands(object): """Class for managing quotas.""" # TODO(melwitt): Remove this during the Queens cycle description = ('DEPRECATED: The quota commands are deprecated since ' 'Pike as quota usage is counted from resources instead ' 'of being tracked separately. They will be removed in an ' 'upcoming release.') @args('--project', dest='project_id', metavar='', help='Project Id', required=True) @args('--user', dest='user_id', metavar='', help='User Id') @args('--key', metavar='', help='Key') def refresh(self, project_id, user_id=None, key=None): """DEPRECATED: This command is deprecated and no longer does anything. """ pass class ProjectCommands(object): """Class for managing projects.""" # TODO(stephenfin): Remove this during the Queens cycle description = ('DEPRECATED: The project commands are deprecated since ' 'Pike as this information is available over the API. They ' 'will be removed in an upcoming release.') @args('--project', dest='project_id', metavar='', help='Project name') @args('--user', dest='user_id', metavar='', help='User name') @args('--key', metavar='', help='Key') @args('--value', metavar='', help='Value') def quota(self, project_id, user_id=None, key=None, value=None): """Create, update or display quotas for project/user If no quota key is provided, the quota will be displayed. If a valid quota key is provided and it does not exist, it will be created. Otherwise, it will be updated. """ ctxt = context.get_admin_context() if user_id: quota = QUOTAS.get_user_quotas(ctxt, project_id, user_id) else: user_id = None quota = QUOTAS.get_project_quotas(ctxt, project_id) # if key is None, that means we need to show the quotas instead # of updating them if key: settable_quotas = QUOTAS.get_settable_quotas(ctxt, project_id, user_id=user_id) if key in quota: minimum = settable_quotas[key]['minimum'] maximum = settable_quotas[key]['maximum'] if value.lower() == 'unlimited': value = -1 if int(value) < -1: print(_('Quota limit must be -1 or greater.')) return 2 if ((int(value) < minimum) and (maximum != -1 or (maximum == -1 and int(value) != -1))): print(_('Quota limit must be greater than %s.') % minimum) return 2 if maximum != -1 and int(value) > maximum: print(_('Quota limit must be less than %s.') % maximum) return 2 try: objects.Quotas.create_limit(ctxt, project_id, key, value, user_id=user_id) except exception.QuotaExists: objects.Quotas.update_limit(ctxt, project_id, key, value, user_id=user_id) else: print(_('%(key)s is not a valid quota key. Valid options are: ' '%(options)s.') % {'key': key, 'options': ', '.join(quota)}) return 2 print_format = "%-36s %-10s %-10s %-10s" print(print_format % ( _('Quota'), _('Limit'), _('In Use'), _('Reserved'))) # Retrieve the quota after update if user_id: quota = QUOTAS.get_user_quotas(ctxt, project_id, user_id) else: quota = QUOTAS.get_project_quotas(ctxt, project_id) for key, value in quota.items(): if value['limit'] is None or value['limit'] < 0: value['limit'] = 'unlimited' print(print_format % (key, value['limit'], value['in_use'], value['reserved'])) @args('--project', dest='project_id', metavar='', help='Project Id', required=True) @args('--user', dest='user_id', metavar='', help='User Id') @args('--key', metavar='', help='Key') def quota_usage_refresh(self, project_id, user_id=None, key=None): """DEPRECATED: This command is deprecated and no longer does anything. """ # TODO(melwitt): Remove this during the Queens cycle pass class AccountCommands(ProjectCommands): """Class for managing projects.""" # TODO(stephenfin): Remove this during the Queens cycle description = ('DEPRECATED: The account commands are deprecated since ' 'Pike as this information is available over the API. They ' 'will be removed in an upcoming release.') class FloatingIpCommands(object): """Class for managing floating IP.""" # TODO(stephenfin): Remove these when we remove cells v1 description = ('DEPRECATED: Floating IP commands are deprecated since ' 'nova-network is deprecated in favor of Neutron. The ' 'floating IP commands will be removed in an upcoming ' 'release.') @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) elif net.size >= 1000000: # NOTE(dripton): If we generate a million IPs and put them in # the database, the system will slow to a crawl and/or run # out of memory and crash. This is clearly a misconfiguration. reason = _("Too many IP addresses will be generated. Please " "increase /%s to reduce the number generated." ) % net.prefixlen raise exception.InvalidInput(reason=reason) else: return net.iter_hosts() @args('--ip_range', metavar='', help='IP range') @args('--pool', metavar='', help='Optional pool') @args('--interface', metavar='', 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, want_result=False) 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) return 1 @args('--ip_range', metavar='', 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', metavar='', 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'])) @decorator.decorator def validate_network_plugin(f, *args, **kwargs): """Decorator to validate the network plugin.""" if utils.is_neutron(): print(_("ERROR: Network commands are not supported when using the " "Neutron API. Use python-neutronclient instead.")) return 2 return f(*args, **kwargs) class NetworkCommands(object): """Class for managing networks.""" # TODO(stephenfin): Remove these when we remove cells v1 description = ('DEPRECATED: Network commands are deprecated since ' 'nova-network is deprecated in favor of Neutron. The ' 'network commands will be removed in an upcoming release.') @validate_network_plugin @args('--label', metavar='