#!/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 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.db.sqlalchemy import session as db_session 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 servicegroup 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.floating_ips') 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='', help='Project name') @args('--ip', dest="ip", metavar='', help='IP Address') @args('--port', dest="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', dest="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: 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='', 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.") exit(1) class ProjectCommands(object): """Class for managing projects.""" @args('--project', dest="project_id", metavar='', help='Project name') @args('--key', dest="key", metavar='', help='Key') @args('--value', dest="value", metavar='', 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='', 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='', 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='', help='IP address') def reserve(self, address): """Mark fixed ip as reserved arguments: address""" self._set_reserved(address, True) @args('--address', dest="address", metavar='', 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='', help='IP range') @args('--pool', dest="pool", metavar='', help='Optional pool') @args('--interface', dest="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) 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='', 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='', 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='