# 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 os import sys 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 six import six.moves.urllib.parse as urlparse from nova.api.ec2 import ec2utils from nova import availability_zones 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 flavor as flavor_obj from nova.objects import instance as instance_obj from nova.objects import keypair as keypair_obj from nova.objects import request_spec from nova import quota from nova import rpc from nova import utils from nova import version CONF = nova.conf.CONF QUOTAS = quota.QUOTAS _EXTRA_DEFAULT_LOG_LEVELS = ['oslo_db=INFO'] # 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.""" description = ('DEPRECATED: VPN commands are deprecated since ' 'nova-network is deprecated in favor of Neutron. The ' 'VPN commands will be removed in the Nova 15.0.0 ' 'Ocata release.') @args('--project', dest='project_id', metavar='', help='Project name') @args('--ip', metavar='', help='IP Address') @args('--port', metavar='', 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', 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 ProjectCommands(object): """Class for managing projects.""" @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: db.quota_create(ctxt, project_id, key, value, user_id=user_id) except exception.QuotaExists: db.quota_update(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 six.iteritems(quota): if value['limit'] < 0 or value['limit'] is None: 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): """Refresh the quotas for project/user If no quota key is provided, all the quota usages will be refreshed. If a valid quota key is provided and it does not exist, it will be created. Otherwise, it will be refreshed. """ ctxt = context.get_admin_context() keys = None if key: keys = [key] try: QUOTAS.usage_refresh(ctxt, project_id, user_id, keys) except exception.QuotaUsageRefreshNotAllowed as e: print(e.format_message()) return 2 @args('--project', dest='project_id', metavar='', help='Project name') def scrub(self, project_id): """DEPRECATED: Deletes network data associated with project. This command is only for nova-network deployments and nova-network is deprecated in favor of Neutron. This command will be removed in the Nova 15.0.0 Ocata release. """ 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.""" description = ('DEPRECATED: Fixed IP commands are deprecated since ' 'nova-network is deprecated in favor of Neutron. The ' 'fixed IP commands will be removed in the Nova 15.0.0 ' 'Ocata release.') @args('--host', metavar='', 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_by_host(ctxt, host) except exception.NotFound as ex: print(_("error: %s") % ex) return(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', metavar='', help='IP address') def reserve(self, address): """Mark fixed IP as reserved arguments: address """ return self._set_reserved(address, True) @args('--address', metavar='', help='IP address') def unreserve(self, address): """Mark fixed IP as free to use arguments: address """ return 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) return(2) class FloatingIpCommands(object): """Class for managing floating IP.""" description = ('DEPRECATED: Floating IP commands are deprecated since ' 'nova-network is deprecated in favor of Neutron. The ' 'floating IP commands will be removed in the Nova 15.0.0 ' 'Ocata 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.""" description = ('DEPRECATED: Network commands are deprecated since ' 'nova-network is deprecated in favor of Neutron. The ' 'network commands will be removed in the Nova 15.0.0 Ocata ' 'release.') @validate_network_plugin @args('--label', metavar='