#!/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 ast import errno import gettext import glob import json import math import netaddr import optparse import os import StringIO import sys import time # 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.compat import flagfile from nova import context from nova import crypto from nova import db from nova import exception from nova import flags from nova import image from nova import log as logging from nova import quota from nova import rpc from nova import utils from nova import version from nova import vsa from nova.api.ec2 import ec2utils from nova.auth import manager from nova.compute import instance_types from nova.compute import vm_states from nova.db import migration from nova.volume import volume_types FLAGS = flags.FLAGS flags.DECLARE('flat_network_bridge', 'nova.network.manager') flags.DECLARE('num_networks', 'nova.network.manager') flags.DECLARE('multi_host', 'nova.network.manager') flags.DECLARE('network_size', 'nova.network.manager') flags.DECLARE('vlan_start', 'nova.network.manager') flags.DECLARE('vpn_start', 'nova.network.manager') flags.DECLARE('default_floating_pool', 'nova.network.manager') flags.DECLARE('public_interface', 'nova.network.linux_net') # Decorators for actions def args(*args, **kwargs): def _decorator(func): func.__dict__.setdefault('options', []).insert(0, (args, kwargs)) return func return _decorator def param2id(object_id): """Helper function to convert various id types to internal id. args: [object_id], e.g. 'vol-0000000a' or 'volume-0000000a' or '10' """ if '-' in object_id: return ec2utils.ec2_id_to_id(object_id) else: return int(object_id) class VpnCommands(object): """Class for managing VPNs.""" def __init__(self): self.manager = manager.AuthManager() @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 specifed path with flags set properly. arguments: path""" exec(compile(open(path).read(), path, 'exec'), locals(), globals()) @args('--filename', dest='filename', metavar='', default=False, help='Export file path') def export(self, filename): """Export Nova users into a file that can be consumed by Keystone""" def create_file(filename): data = generate_data() with open(filename, 'w') as f: f.write(data.getvalue()) def tenants(data, am): for project in am.get_projects(): print >> data, ("tenant add '%s'" % (project.name)) for u in project.member_ids: user = am.get_user(u) print >> data, ("user add '%s' '%s' '%s'" % (user.name, user.access, project.name)) print >> data, ("credentials add 'EC2' '%s:%s' '%s' '%s'" % (user.access, project.id, user.secret, project.id)) def roles(data, am): for role in am.get_roles(): print >> data, ("role add '%s'" % (role)) def grant_roles(data, am): roles = am.get_roles() for project in am.get_projects(): for u in project.member_ids: user = am.get_user(u) for role in db.user_get_roles_for_project(ctxt, u, project.id): print >> data, ("role grant '%s', '%s', '%s')," % (user.name, role, project.name)) print >> data def generate_data(): data = StringIO.StringIO() am = manager.AuthManager() tenants(data, am) roles(data, am) grant_roles(data, am) data.seek(0) return data ctxt = context.get_admin_context() if filename: create_file(filename) else: data = generate_data() print data.getvalue() class RoleCommands(object): """Class for managing roles.""" def __init__(self): self.manager = manager.AuthManager() @args('--user', dest="user", metavar='', help='User name') @args('--role', dest="role", metavar='', help='User role') @args('--project', dest="project", metavar='', help='Project name') def add(self, user, role, project=None): """adds role to user if project is specified, adds project specific role""" if project: projobj = self.manager.get_project(project) if not projobj.has_member(user): print "%s not a member of %s" % (user, project) return self.manager.add_role(user, role, project) @args('--user', dest="user", metavar='', help='User name') @args('--role', dest="role", metavar='', help='User role') @args('--project', dest="project", metavar='', help='Project name') def has(self, user, role, project=None): """checks to see if user has role if project is specified, returns True if user has the global role and the project role""" print self.manager.has_role(user, role, project) @args('--user', dest="user", metavar='', help='User name') @args('--role', dest="role", metavar='', help='User role') @args('--project', dest="project", metavar='', help='Project name') def remove(self, user, role, project=None): """removes role from user if project is specified, removes project specific role""" self.manager.remove_role(user, role, project) 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 UserCommands(object): """Class for managing users.""" @staticmethod def _print_export(user): """Print export variables to use with API.""" print 'export EC2_ACCESS_KEY=%s' % user.access print 'export EC2_SECRET_KEY=%s' % user.secret def __init__(self): self.manager = manager.AuthManager() @args('--name', dest="name", metavar='', help='Admin name') @args('--access', dest="access", metavar='', help='Access') @args('--secret', dest="secret", metavar='', help='Secret') def admin(self, name, access=None, secret=None): """creates a new admin and prints exports""" try: user = self.manager.create_user(name, access, secret, True) except exception.DBError, e: _db_error(e) self._print_export(user) @args('--name', dest="name", metavar='', help='User name') @args('--access', dest="access", metavar='', help='Access') @args('--secret', dest="secret", metavar='', help='Secret') def create(self, name, access=None, secret=None): """creates a new user and prints exports""" try: user = self.manager.create_user(name, access, secret, False) except exception.DBError, e: _db_error(e) self._print_export(user) @args('--name', dest="name", metavar='', help='User name') def delete(self, name): """deletes an existing user arguments: name""" self.manager.delete_user(name) @args('--name', dest="name", metavar='', help='User name') def exports(self, name): """prints access and secrets for user in export format""" user = self.manager.get_user(name) if user: self._print_export(user) else: print "User %s doesn't exist" % name def list(self): """lists all users""" for user in self.manager.get_users(): print user.name @args('--name', dest="name", metavar='', help='User name') @args('--access', dest="access_key", metavar='', help='Access key') @args('--secret', dest="secret_key", metavar='', help='Secret key') @args('--is_admin', dest='is_admin', metavar="<'T'|'F'>", help='Is admin?') def modify(self, name, access_key, secret_key, is_admin): """update a users keys & admin flag arguments: accesskey secretkey admin leave any field blank to ignore it, admin should be 'T', 'F', or blank """ if not is_admin: is_admin = None elif is_admin.upper()[0] == 'T': is_admin = True else: is_admin = False self.manager.modify_user(name, access_key, secret_key, is_admin) @args('--name', dest="user_id", metavar='', help='User name') @args('--project', dest="project_id", metavar='', help='Project name') def revoke(self, user_id, project_id=None): """revoke certs for a user""" if project_id: crypto.revoke_certs_by_user_and_project(user_id, project_id) else: crypto.revoke_certs_by_user(user_id) class ProjectCommands(object): """Class for managing projects.""" def __init__(self): self.manager = manager.AuthManager() @args('--project', dest="project_id", metavar='', help='Project name') @args('--user', dest="user_id", metavar='', help='User name') def add(self, project_id, user_id): """Adds user to project""" try: self.manager.add_to_project(user_id, project_id) except exception.UserNotFound as ex: print ex raise @args('--project', dest="name", metavar='', help='Project name') @args('--user', dest="project_manager", metavar='', help='Project manager') @args('--desc', dest="description", metavar='', help='Description') def create(self, name, project_manager, description=None): """Creates a new project""" try: self.manager.create_project(name, project_manager, description) except exception.UserNotFound as ex: print ex raise @args('--project', dest="name", metavar='', help='Project name') @args('--user', dest="project_manager", metavar='', help='Project manager') @args('--desc', dest="description", metavar='', help='Description') def modify(self, name, project_manager, description=None): """Modifies a project""" try: self.manager.modify_project(name, project_manager, description) except exception.UserNotFound as ex: print ex raise @args('--project', dest="name", metavar='', help='Project name') def delete(self, name): """Deletes an existing project""" try: self.manager.delete_project(name) except exception.ProjectNotFound as ex: print ex raise @args('--project', dest="project_id", metavar='', help='Project name') @args('--user', dest="user_id", metavar='', help='User name') @args('--file', dest="filename", metavar='', help='File name(Default: novarc)') def environment(self, project_id, user_id, filename='novarc'): """Exports environment variables to an sourcable file""" try: rc = self.manager.get_environment_rc(user_id, project_id) except (exception.UserNotFound, exception.ProjectNotFound) as ex: print ex raise if filename == "-": sys.stdout.write(rc) else: with open(filename, 'w') as f: f.write(rc) @args('--user', dest="username", metavar='', help='User name') def list(self, username=None): """Lists all projects""" for project in self.manager.get_projects(username): print project.name @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() if key: if value.lower() == 'unlimited': value = None try: db.quota_update(ctxt, project_id, key, value) except exception.ProjectQuotaNotFound: db.quota_create(ctxt, project_id, key, value) project_quota = quota.get_project_quotas(ctxt, project_id) for key, value in project_quota.iteritems(): if value is None: value = 'unlimited' print '%s: %s' % (key, value) @args('--project', dest="project_id", metavar='', help='Project name') @args('--user', dest="user_id", metavar='', help='User name') def remove(self, project_id, user_id): """Removes user from project""" try: self.manager.remove_from_project(user_id, project_id) except (exception.UserNotFound, exception.ProjectNotFound) as ex: print ex raise @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']) @args('--project', dest="project_id", metavar='', help='Project name') @args('--user', dest="user_id", metavar='', help='User name') @args('--file', dest="filename", metavar='', help='File name(Default: nova.zip)') def zipfile(self, project_id, user_id, filename='nova.zip'): """Exports credentials for project to a zip file""" try: zip_file = self.manager.get_credentials(user_id, project_id) if filename == "-": sys.stdout.write(zip_file) else: with open(filename, 'w') as f: f.write(zip_file) except (exception.UserNotFound, exception.ProjectNotFound) as ex: print ex raise except db.api.NoMoreNetworks: print _('No more networks available. If this is a new ' 'installation, you need\nto call something like this:\n\n' ' nova-manage network create pvt 10.0.0.0/8 10 64\n\n') except exception.ProcessExecutionError, e: print e print _("The above error may show that the certificate db has " "not been created.\nPlease create a database by running " "a nova-cert server on this host.") 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_id = {} for instance in instances: instances_by_id[instance['id']] = instance print "%-18s\t%-15s\t%-15s\t%s" % (_('network'), _('IP address'), _('hostname'), _('host')) for fixed_ip in fixed_ips: hostname = None host = None mac_address = None network = db.network_get(context.get_admin_context(), fixed_ip['network_id']) if fixed_ip['instance_id']: instance = instances_by_id[fixed_ip['instance_id']] hostname = instance['hostname'] host = instance['host'] print "%-18s\t%-15s\t%-15s\t%s" % ( network['cidr'], fixed_ip['address'], hostname, host) @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 network_to_host_list(network): """Return the list of host of a network. If the network is empty and only has one host, then we consider it has the host in the network. """ if network.size == 1: yield network.ip else: for host in network.iter_hosts(): yield host @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""" addresses = netaddr.IPNetwork(ip_range) admin_context = context.get_admin_context() if not pool: pool = FLAGS.default_floating_pool if not interface: interface = FLAGS.public_interface for address in self.network_to_host_list(addresses): db.floating_ip_create(admin_context, {'address': str(address), 'pool': pool, 'interface': interface}) @args('--ip_range', dest="ip_range", metavar='', help='IP range') def delete(self, ip_range): """Deletes floating ips by range""" for address in self.network_to_host_list(netaddr.IPNetwork(ip_range)): db.floating_ip_destroy(context.get_admin_context(), str(address)) @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_id = None if floating_ip['fixed_ip_id']: fixed_ip = db.fixed_ip_get(ctxt, floating_ip['fixed_ip_id']) instance = db.instance_get(ctxt, fixed_ip['instance_id']) instance_id = instance.get('uuid', "none") print "%s\t%s\t%s\t%s\t%s" % (floating_ip['project_id'], floating_ip['address'], instance_id, floating_ip['pool'], floating_ip['interface']) class NetworkCommands(object): """Class for managing networks.""" @args('--label', dest="label", metavar='