[pkg-eucalyptus-commits] [SCM] managing cloud instances for Eucalyptus branch, master, updated. 3.0.0-alpha3-257-g1da8e3a

Garrett Holmstrom gholms at fedoraproject.org
Sun Jun 16 02:30:41 UTC 2013


The following commit has been merged in the master branch:
commit 7cdceb98df0e34a6f614c4f3f9dd56770f4ae800
Author: Garrett Holmstrom <gholms at fedoraproject.org>
Date:   Sun Mar 17 01:52:14 2013 -0700

    Bump to EC2 API 2013-02-01

diff --git a/euca2ools/commands/argtypes.py b/euca2ools/commands/argtypes.py
index 40ee8dc..6674334 100644
--- a/euca2ools/commands/argtypes.py
+++ b/euca2ools/commands/argtypes.py
@@ -33,6 +33,7 @@ import base64
 from requestbuilder import EMPTY
 import sys
 
+
 def ec2_block_device_mapping(map_as_str):
     '''
     Parse a block device mapping from an image registration command line.
@@ -41,8 +42,8 @@ def ec2_block_device_mapping(map_as_str):
         (device, mapping) = map_as_str.split('=')
     except ValueError:
         raise argparse.ArgumentTypeError(
-                'block device mapping "{0}" must have form '
-                'DEVICE=MAPPED'.format(map_as_str))
+            'block device mapping "{0}" must have form DEVICE=MAPPED'
+            .format(map_as_str))
     map_dict = {'DeviceName': device}
     if mapping.lower() == 'none':
         map_dict['NoDevice'] = 'none'
@@ -51,14 +52,13 @@ def ec2_block_device_mapping(map_as_str):
     elif (mapping.startswith('snap-') or mapping.startswith('vol-') or
           mapping.startswith(':')):
         map_bits = mapping.split(':')
-        if len(map_bits) == 1:
-            map_bits.append(None)
-        if len(map_bits) == 2:
+        while len(map_bits) < 5:
             map_bits.append(None)
-        if len(map_bits) != 3:
+        if len(map_bits) != 5:
             raise argparse.ArgumentTypeError(
-                    'EBS block device mapping "{0}" must have form '
-                    'DEVICE=[SNAP-ID]:[SIZE]:[true|false]'.format(map_as_str))
+                'EBS block device mapping "{0}" must have form '
+                'DEVICE=[SNAP-ID]:[SIZE]:[true|false]:[standard|TYPE[:IOPS]]'
+                .format(map_as_str))
 
         map_dict['Ebs'] = {}
         if map_bits[0]:
@@ -68,29 +68,138 @@ def ec2_block_device_mapping(map_as_str):
                 map_dict['Ebs']['VolumeSize'] = int(map_bits[1])
             except ValueError:
                 raise argparse.ArgumentTypeError(
-                        'second element of EBS block device mapping "{0}" '
-                        'must be an integer'.format(map_as_str))
+                    'second element of EBS block device mapping "{0}" must be '
+                    'an integer'.format(map_as_str))
         if map_bits[2]:
             if map_bits[2].lower() not in ('true', 'false'):
                 raise argparse.ArgumentTypeError(
-                        'third element of EBS block device mapping "{0}" must '
-                        'be "true" or "false"'.format(map_as_str))
+                    'third element of EBS block device mapping "{0}" must be '
+                    '"true" or "false"'.format(map_as_str))
             map_dict['Ebs']['DeleteOnTermination'] = map_bits[2].lower()
+        if map_bits[3]:
+            map_dict['Ebs']['VolumeType'] = map_bits[3]
+        if map_bits[4]:
+            if map_bits[3] == 'standard':
+                raise argparse.ArgumentTypeError(
+                    'fifth element of EBS block device mapping "{0}" is not '
+                    'allowed with volume type "standard"'.format(map_as_str))
+            map_dict['Ebs']['Iops'] = map_bits[4]
         if not map_dict['Ebs']:
             raise argparse.ArgumentTypeError(
-                    'EBS block device mapping "{0}" must specify at least one '
-                    'element.  Use "{1}=none" to specify that no device '
-                    'should be mapped.'.format(map_as_str, device))
+                'EBS block device mapping "{0}" must specify at least one '
+                'element.  Use "{1}=none" to suppress an existing mapping.'
+                .format(map_as_str, device))
     elif not mapping:
         raise argparse.ArgumentTypeError(
-                'invalid block device mapping "{0}".  Use "{1}=none" to '
-                'specify that no device should be mapped.'.format(map_as_str,
-                                                                  device))
+            'invalid block device mapping "{0}".  Use "{1}=none" to suppress '
+            'an existing mapping.'.format(map_as_str, device))
     else:
         raise argparse.ArgumentTypeError(
-                'invalid block device mapping "{0}"'.format(map_as_str))
+            'invalid block device mapping "{0}"'.format(map_as_str))
     return map_dict
 
+
+def vpc_interface(iface_as_str):
+    '''
+    Nine-part VPC network interface definition:
+    [INTERFACE]:INDEX:[SUBNET]:[DESCRIPTION]:[PRIV_IP]:[GROUP1,GROUP2,...]:
+    [true|false]:[SEC_IP_COUNT|:SEC_IP1,SEC_IP2,...]
+    '''
+
+    if len(iface_as_str) == 0:
+        raise argparse.ArgumentTypeError(
+            'network interface definitions must be non-empty'.format(
+                iface_as_str))
+
+    bits = iface_as_str.split(':')
+    iface = {}
+
+    if len(bits) < 2:
+        raise argparse.ArgumentTypeError(
+            'network interface definition "{0}" must consist of at least 2 '
+            'elements ({1} provided)'.format(iface_as_str, len(bits)))
+    elif len(bits) > 9:
+        raise argparse.ArgumentTypeError(
+            'network interface definition "{0}" must consist of at most 9 '
+            'elements ({1} provided)'.format(iface_as_str, len(bits)))
+    while len(bits) < 9:
+        bits.append(None)
+
+    if bits[0]:
+        # Preexisting NetworkInterfaceId
+        if bits[0].startswith('eni-') and len(bits[0]) == 12:
+            iface['NetworkInterfaceId'] = bits[0]
+        else:
+            raise argparse.ArgumentTypeError(
+                'first element of network interface definition "{0}" must be '
+                'a network interface ID'.format(iface_as_str))
+    if bits[1]:
+        # DeviceIndex
+        try:
+            iface['DeviceIndex'] = int(bits[1])
+        except ValueError:
+            raise argparse.ArgumentTypeError(
+                'second element of network interface definition "{0}" must be '
+                'an integer'.format(iface_as_str))
+    else:
+        raise argparse.ArgumentTypeError(
+            'second element of network interface definition "{0}" must be '
+            'non-empty'.format(iface_as_str))
+    if bits[2]:
+        # SubnetId
+        if bits[2].startswith('subnet-'):
+            iface['SubnetId'] = bits[2]
+        else:
+            raise argparse.ArgumentTypeError(
+                'third element of network interface definition "{0}" must be '
+                'a subnet ID'.format(iface_as_str))
+    if bits[3]:
+        # Description
+        iface['Description'] = bits[3]
+    if bits[4]:
+        # PrivateIpAddresses.n.PrivateIpAddress
+        # PrivateIpAddresses.n.Primary
+        iface.setdefault('PrivateIpAddresses', [])
+        iface['PrivateIpAddresses'].append({'PrivateIpAddress': bits[4],
+                                            'Primary': 'true'})
+    if bits[5]:
+        # SecurityGroupId.n
+        groups = filter(None, bits[5].split(','))
+        if not all(group.startswith('sg-') for group in groups):
+            raise argparse.ArgumentTypeError(
+                'sixth element of network interface definition "{0}" must '
+                'refer to security groups by IDs, not names'
+                .format(iface_as_str))
+        iface['SecurityGroupId'] = groups
+    if bits[6]:
+        # DeleteOnTermination
+            if bits[6] in ('true', 'false'):
+                iface['DeleteOnTermination'] = bits[6]
+            else:
+                raise argparse.ArgumentTypeError(
+                    'seventh element of network interface definition "{0}" '
+                    'must be "true" or "false"'.format(iface_as_str))
+    if bits[7]:
+        # SecondaryPrivateIpAddressCount
+        if bits[8]:
+            raise argparse.ArgumentTypeError(
+                'eighth and ninth elements of network interface definition '
+                '"{0}" must not both be non-empty'.format(iface_as_str))
+        try:
+            iface['SecondaryPrivateIpAddressCount'] = int(bits[7])
+        except ValueError:
+            raise argparse.ArgumentTypeError(
+                'eighth element of network interface definition "{0}" must be '
+                'an integer'.format(iface_as_str))
+    if bits[8]:
+        # PrivateIpAddresses.n.PrivateIpAddress
+            sec_ips = [{'PrivateIpAddress': addr} for addr in
+                       bits[8].split(',') if addr]
+            iface.setdefault('PrivateIpAddresses', [])
+            iface['PrivateIpAddresses'].extend(sec_ips)
+    return iface
+
+
 def file_contents(filename):
     if filename == '-':
         return sys.stdin.read()
@@ -98,6 +207,7 @@ def file_contents(filename):
         with open(filename) as arg_file:
             return arg_file.read()
 
+
 def b64encoded_file_contents(filename):
     if filename == '-':
         return base64.b64encode(sys.stdin.read())
@@ -105,6 +215,7 @@ def b64encoded_file_contents(filename):
         with open(filename) as arg_file:
             return base64.b64encode(arg_file.read())
 
+
 def binary_tag_def(tag_str):
     '''
     Parse a tag definition from the command line.  Return a dict that depends
@@ -120,6 +231,7 @@ def binary_tag_def(tag_str):
     else:
         return {'Key': tag_str, 'Value': EMPTY}
 
+
 def ternary_tag_def(tag_str):
     '''
     Parse a tag definition from the command line.  Return a dict that depends
@@ -135,6 +247,7 @@ def ternary_tag_def(tag_str):
     else:
         return {'Key': tag_str}
 
+
 def delimited_list(delimiter):
     def _concrete_delimited_list(list_as_str):
         if isinstance(list_as_str, str) and len(list_as_str) > 0:
diff --git a/euca2ools/commands/euca/__init__.py b/euca2ools/commands/euca/__init__.py
index cd2e02f..9f8df66 100644
--- a/euca2ools/commands/euca/__init__.py
+++ b/euca2ools/commands/euca/__init__.py
@@ -29,6 +29,7 @@
 # POSSIBILITY OF SUCH DAMAGE.
 
 import argparse
+from euca2ools.commands import Euca2ools
 from euca2ools.exceptions import AWSError
 from operator import itemgetter
 import os.path
@@ -42,7 +43,7 @@ import requests
 import shlex
 from string import Template
 import sys
-from .. import Euca2ools
+
 
 class EC2CompatibleQuerySigV2Auth(QuerySigV2Auth):
     # -a and -s are deprecated; remove them in 3.2
@@ -133,7 +134,7 @@ class EC2CompatibleQuerySigV2Auth(QuerySigV2Auth):
 class Eucalyptus(requestbuilder.service.BaseService):
     NAME = 'ec2'
     DESCRIPTION = 'Eucalyptus compute cloud service'
-    API_VERSION = '2009-11-30'
+    API_VERSION = '2013-02-01'
     AUTH_CLASS  = EC2CompatibleQuerySigV2Auth
     URL_ENVVAR = 'EC2_URL'
 
@@ -241,7 +242,7 @@ class EucalyptusRequest(requestbuilder.request.AWSQueryRequest,
         instance_line.append(instance.get('placement', {}).get('availabilityZone'))
         instance_line.append(instance.get('kernelId'))
         instance_line.append(instance.get('ramdiskId'))
-        instance_line.append(None)  # What is this?
+        instance_line.append(instance.get('platform'))
         if instance.get('monitoring'):
             instance_line.append('monitoring-' +
                                  instance['monitoring'].get('state'))
@@ -252,35 +253,76 @@ class EucalyptusRequest(requestbuilder.request.AWSQueryRequest,
         instance_line.append(instance.get('vpcId'))
         instance_line.append(instance.get('subnetId'))
         instance_line.append(instance.get('rootDeviceType'))
-        instance_line.append(None)  # What is this?
-        instance_line.append(None)  # What is this?
-        instance_line.append(None)  # What is this?
-        instance_line.append(None)  # What is this?
+        instance_line.append(instance.get('instanceLifecycle'))
+        instance_line.append(instance.get('showInstanceRequestId'))
+        instance_line.append(None)  # Should be the license, but where is it?
+        instance_line.append(instance.get('placement', {}).get('groupName'))
         instance_line.append(instance.get('virtualizationType'))
         instance_line.append(instance.get('hypervisor'))
-        instance_line.append(None)  # What is this?
-        instance_line.append(instance.get('placement', {}).get('groupName'))
+        instance_line.append(instance.get('clientToken'))
         instance_line.append(','.join([group['groupId'] for group in
                                        instance.get('groupSet', [])]))
         instance_line.append(instance.get('placement', {}).get('tenancy'))
+        instance_line.append(instance.get('ebsOptimized'))
+        instance_line.append(instance.get('iamInstanceProfile', {}).get('arn'))
         print self.tabify(instance_line)
 
         for blockdev in instance.get('blockDeviceMapping', []):
             self.print_blockdevice(blockdev)
 
+        for nic in instance.get('networkInterfaceSet', []):
+            self.print_interface(nic)
+
         for tag in instance.get('tagSet', []):
             self.print_resource_tag(tag, instance.get('instanceId'))
 
     def print_blockdevice(self, blockdev):
-        print self.tabify(['BLOCKDEVICE', blockdev.get('deviceName'),
+        print self.tabify(('BLOCKDEVICE', blockdev.get('deviceName'),
                            blockdev.get('ebs', {}).get('volumeId'),
                            blockdev.get('ebs', {}).get('attachTime'),
-                           blockdev.get('ebs', {}).get('deleteOnTermination')])
+                           blockdev.get('ebs', {}).get('deleteOnTermination'),
+                           blockdev.get('ebs', {}).get('volumeType'),
+                           blockdev.get('ebs', {}).get('iops')))
+
+    def print_interface(self, nic):
+        nic_info = [nic.get(attr) for attr in ('networkInterfaceId',
+            'subnetId', 'vpcId', 'ownerId', 'status', 'privateIpAddress',
+            'privateDnsName', 'sourceDestCheck')]
+        print self.tabify(['NIC'] + nic_info)
+        for attachment in nic.get('attachment', []):
+            attachment_info = [attachment.get(attr) for attr in (
+                'attachmentID', 'deviceIndex', 'status', 'attachTime',
+                'deleteOnTermination')]
+            print self.tabify(['NICATTACHMENT'] + attachment_info)
+        privaddresses = nic.get('privateIpAddressesSet', [])
+        for association in nic.get('association', []):
+            # The EC2 tools apparently print private IP info in the
+            # association even though that info doesn't appear there
+            # in the response, so we have to look it up elsewhere.
+            for privaddress in privaddresses:
+                if (privaddress.get('association', {}).get('publicIp') ==
+                    association.get('publicIp')):
+                    # Found a match
+                    break
+            else:
+                privaddress = None
+            print self.tabify(('NICASSOCIATION', association.get('publicIp'),
+                               association.get('ipOwnerId'), privaddress))
+        for group in nic.get('groupSet', []):
+            print self.tabify(('GROUP', group.get('groupId'),
+                               group.get('groupName')))
+        for privaddress in privaddresses:
+            print self.tabify(('PRIVATEIPADDRESS',
+                               privaddress.get('privateIpAddress')))
 
     def print_volume(self, volume):
-        print self.tabify(['VOLUME'] + [volume.get(attr) for attr in
-                ('volumeId', 'size', 'snapshotId', 'availabilityZone',
-                 'status', 'createTime')])
+        vol_bits = ['VOLUME']
+        for attr in ('volumeId', 'size', 'snapshotId', 'availabilityZone',
+                     'status', 'createTime'):
+            vol_bits.append(volume.get(attr))
+        vol_bits.append(volume.get('volumeType') or 'standard')
+        vol_bits.append(volume.get('iops'))
+        print self.tabify(vol_bits)
         for attachment in volume.get('attachmentSet', []):
             self.print_attachment(attachment)
         for tag in volume.get('tagSet', []):
diff --git a/euca2ools/commands/euca/allocateaddress.py b/euca2ools/commands/euca/allocateaddress.py
index cd7a837..ff474e4 100644
--- a/euca2ools/commands/euca/allocateaddress.py
+++ b/euca2ools/commands/euca/allocateaddress.py
@@ -28,11 +28,17 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-import euca2ools.commands.euca
+from euca2ools.commands.euca import EucalyptusRequest
+from requestbuilder import Arg
 
-class AllocateAddress(euca2ools.commands.euca.EucalyptusRequest):
+
+class AllocateAddress(EucalyptusRequest):
     DESCRIPTION = 'Allocate a public IP address'
+    ARGS = [Arg('-d', '--domain', dest='Domain', metavar='vpc',
+                choices=('vpc',), help='''[VPC only] "vpc" to allocate the
+                address for use in a VPC''')]
 
     def print_result(self, result):
         print self.tabify(('ADDRESS', result.get('publicIp'),
-                           result.get('domain'), result.get('allocationId')))
+                           result.get('domain', 'standard'),
+                           result.get('allocationId')))
diff --git a/euca2ools/commands/euca/associateaddress.py b/euca2ools/commands/euca/associateaddress.py
index 3aae1f2..02d91d9 100644
--- a/euca2ools/commands/euca/associateaddress.py
+++ b/euca2ools/commands/euca/associateaddress.py
@@ -28,16 +28,54 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-from requestbuilder import Arg
-from . import EucalyptusRequest
+from euca2ools.commands.euca import EucalyptusRequest
+from requestbuilder import Arg, MutuallyExclusiveArgList
+from requestbuilder.exceptions import ArgumentError
+
 
 class AssociateAddress(EucalyptusRequest):
     DESCRIPTION = 'Associate an elastic IP address with a running instance'
-    ARGS = [Arg('-i', '--instance', dest='InstanceId', metavar='INSTANCE',
-                required=True, help='instance to associate the address with'),
-            Arg('PublicIp', metavar='ADDRESS', help='IP address to associate')]
+    ARGS = [MutuallyExclusiveArgList(True,
+                Arg('-i', '--instance-id', dest='InstanceId',
+                    metavar='INSTANCE', help='''ID of the instance to associate
+                    the address with'''),
+                Arg('-n', '--network-interface', dest='NetworkInterfaceId',
+                    metavar='INTERFACE', help='''[VPC only] network interface
+                    to associate the address with''')),
+            Arg('PublicIp', metavar='ADDRESS', nargs='?', help='''[Non-VPC
+                only] IP address to associate (required)'''),
+            Arg('-a', '--allocation-id', dest='AllocationId', metavar='ALLOC',
+                help='[VPC only] VPC allocation ID (required)'),
+            Arg('-p', '--private-ip-address', dest='PrivateIpAddress',
+                metavar='ADDRESS', help='''[VPC only] the private address to
+                associate with the address being associated in the VPC
+                (default: primary private IP)'''),
+            Arg('--allow-reassociation', dest='AllowReassociation',
+                action='store_const', const='true',
+                help='''[VPC only] allow the address to be associated even if
+                it is already associated with another interface''')]
+
+    def configure(self):
+        EucalyptusRequest.configure(self)
+        if (self.args.get('PublicIp') is not None and
+            self.args.get('AllocationId') is not None):
+            # Can't be both EC2 and VPC
+            raise ArgumentError(
+                'argument -a/--allocation-id: not allowed with an IP address')
+        if (self.args.get('PublicIp') is None and
+            self.args.get('AllocationId') is None):
+            # ...but we still have to be one of them
+            raise ArgumentError(
+                'argument -a/--allocation-id or an IP address is required')
 
     def print_result(self, result):
-        print self.tabify(('ADDRESS', self.args['PublicIp'],
-                           self.args['InstanceId'],
-                           result.get('associationId')))
+        if self.args.get('AllocationId'):
+            # VPC
+            print self.tabify(('ADDRESS', self.args.get('InstanceId'),
+                               self.args.get('AllocationId'),
+                               response.get('associationId'),
+                               self.args.get('PrivateIpAddress')))
+        else:
+            # EC2
+            print self.tabify(('ADDRESS', self.args.get('PublicIp'),
+                               self.args.get('InstanceId')))
diff --git a/euca2ools/commands/euca/attachvolume.py b/euca2ools/commands/euca/attachvolume.py
index c28644c..fe4f9a3 100644
--- a/euca2ools/commands/euca/attachvolume.py
+++ b/euca2ools/commands/euca/attachvolume.py
@@ -28,16 +28,19 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class AttachVolume(EucalyptusRequest):
     DESCRIPTION = 'Attach an EBS volume to an instance'
     ARGS = [Arg('-i', '--instance', dest='InstanceId', metavar='INSTANCE',
-                required=True, help='instance to attach the folume to'),
+                required=True,
+                help='instance to attach the volume to (required)'),
             Arg('-d', '--device', dest='Device', required=True,
-                help='device name exposed to the instance'),
-            Arg('VolumeId', metavar='VOLUME', help='volume to attach')]
+                help='device name exposed to the instance (required)'),
+            Arg('VolumeId', metavar='VOLUME',
+                help='ID of the volume to attach (required)')]
 
     def print_result(self, result):
         self.print_attachment(result)
diff --git a/euca2ools/commands/euca/authorize.py b/euca2ools/commands/euca/authorize.py
index d4405cf..a5bcf56 100644
--- a/euca2ools/commands/euca/authorize.py
+++ b/euca2ools/commands/euca/authorize.py
@@ -28,8 +28,15 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-from .modgroup import ModifySecurityGroupRequest
+from euca2ools.commands.euca.modgroup import ModifySecurityGroupRequest
+
 
 class Authorize(ModifySecurityGroupRequest):
-    NAME = 'AuthorizeSecurityGroupIngress'
-    DESCRIPTION = 'Authorize a rule for a security group'
+    DESCRIPTION = 'Add a rule to a security group that allows traffic to pass'
+
+    @property
+    def action(self):
+        if self.args['egress']:
+            return 'AuthorizeSecurityGroupEgress'
+        else:
+            return 'AuthorizeSecurityGroupIngress'
diff --git a/euca2ools/commands/euca/bundleinstance.py b/euca2ools/commands/euca/bundleinstance.py
index 596ded0..1f13927 100644
--- a/euca2ools/commands/euca/bundleinstance.py
+++ b/euca2ools/commands/euca/bundleinstance.py
@@ -30,34 +30,37 @@
 
 import base64
 from datetime import datetime, timedelta
+from euca2ools.commands.euca import EucalyptusRequest
 import hashlib
 import hmac
 import json
 from requestbuilder import Arg
+from requestbuilder.exceptions import ArgumentError
 import textwrap
-from . import EucalyptusRequest
+
 
 class BundleInstance(EucalyptusRequest):
     DESCRIPTION = 'Bundle an S3-backed Windows instance'
-    ARGS = [Arg('InstanceId', metavar='INSTANCE', help='instance to bundle'),
+    ARGS = [Arg('InstanceId', metavar='INSTANCE',
+                help='ID of the instance to bundle (required)'),
             Arg('-b', '--bucket', dest='Storage.S3.Bucket', metavar='BUCKET',
-                required=True,
-                help='bucket in which to store the new machine image'),
+                required=True, help='''bucket in which to store the new machine
+                image (required)'''),
             Arg('-p', '--prefix', dest='Storage.S3.Prefix', metavar='PREFIX',
                 required=True,
-                help='beginning of the machine image bundle name'),
+                help='beginning of the machine image bundle name (required)'),
             Arg('-o', '--owner-akid', '--user-access-key', metavar='KEY-ID',
                 dest='Storage.S3.AWSAccessKeyId', required=True,
-                help="bucket owner's access key ID"),
+                help="bucket owner's access key ID (required)"),
             Arg('-c', '--policy', metavar='POLICY',
                 dest='Storage.S3.UploadPolicy',
                 help='''Base64-encoded upload policy that allows the server
                         to upload a bundle on your behalf.  If unused, -w is
-                        required'''),
+                        required.'''),
             Arg('-s', '--policy-signature', metavar='SIGNATURE',
                 dest='Storage.S3.UploadPolicySignature',
                 help='''signature of the Base64-encoded upload policy.  If
-                        unused, -w is required'''),
+                        unused, -w is required.'''),
             Arg('-w', '--owner-sak', '--user-secret-key', metavar='KEY',
                 route_to=None,
                 help="""bucket owner's secret access key, used to sign upload
@@ -90,12 +93,12 @@ class BundleInstance(EucalyptusRequest):
         EucalyptusRequest.configure(self)
         if not self.args.get('Storage.S3.UploadPolicy'):
             if not self.args.get('owner_sak'):
-                self._cli_parser.error('argument -w/--owner-sak is required '
-                                       'when -c/--policy is not used')
+                raise ArgumentError('argument -w/--owner-sak is required when '
+                                    '-c/--policy is not used')
         elif not self.args.get('Storage.S3.UploadPolicySignature'):
             if not self.args.get('owner_sak'):
-                self._cli_parser.error('argument -w/--owner-sak is required '
-                                       'when -c/--policy is not used')
+                raise ArgumentError('argument -w/--owner-sak is required when '
+                                    '-s/--policy-signature is not used')
 
     def preprocess(self):
         if not self.args.get('Storage.S3.UploadPolicy'):
diff --git a/euca2ools/commands/euca/cancelbundletask.py b/euca2ools/commands/euca/cancelbundletask.py
index caf5d85..c42c0ff 100644
--- a/euca2ools/commands/euca/cancelbundletask.py
+++ b/euca2ools/commands/euca/cancelbundletask.py
@@ -28,13 +28,14 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class CancelBundleTask(EucalyptusRequest):
     DESCRIPTION = 'Cancel an instance bundling operation'
     ARGS = [Arg('BundleId', metavar='TASK-ID',
-                help='ID of the bundle task to cancel')]
+                help='ID of the bundle task to cancel (required)')]
 
     def print_result(self, result):
         self.print_bundle_task(result.get('bundleInstanceTask'))
diff --git a/euca2ools/commands/euca/confirmproductinstance.py b/euca2ools/commands/euca/confirmproductinstance.py
index 68a8feb..fa2b61b 100644
--- a/euca2ools/commands/euca/confirmproductinstance.py
+++ b/euca2ools/commands/euca/confirmproductinstance.py
@@ -28,15 +28,18 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class ConfirmProductInstance(EucalyptusRequest):
     DESCRIPTION = 'Verify if a product code is associated with an instance'
-    ARGS = [Arg('ProductCode', metavar='CODE', help='product code to confirm'),
+    ARGS = [Arg('ProductCode', metavar='CODE',
+                help='product code to confirm (required)'),
             Arg('-i', '--instance', dest='InstanceId', metavar='INSTANCE',
-                required=True, help='instance to confirm')]
+                required=True,
+                help='ID of the instance to confirm (required)')]
 
     def print_result(self, result):
-        print self.tabify(self.args['ProductCode'], self.args['InstanceId'],
-                          result.get('return'), result.get('ownerId'))
+        print self.tabify((self.args['ProductCode'], self.args['InstanceId'],
+                           result.get('return'), result.get('ownerId')))
diff --git a/euca2ools/commands/euca/createimage.py b/euca2ools/commands/euca/createimage.py
index 034b903..45a4620 100644
--- a/euca2ools/commands/euca/createimage.py
+++ b/euca2ools/commands/euca/createimage.py
@@ -28,21 +28,29 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.argtypes import ec2_block_device_mapping
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class CreateImage(EucalyptusRequest):
     DESCRIPTION = 'Create an EBS image from a running or stopped EBS instance'
     ARGS = [Arg('InstanceId', metavar='INSTANCE',
-                help='instance from which to create the image'),
+                help='instance from which to create the image (required)'),
             Arg('-n', '--name', dest='Name', required=True,
                 help='name for the new image (required)'),
             Arg('-d', '--description', dest='Description', metavar='DESC',
                 help='description for the new image'),
             Arg('--no-reboot', dest='NoReboot', action='store_const',
-                const='true',
-                help='''do not shut down the instance before creating the
-                        image. Image integrity may be affected.''')]
+                const='true', help='''do not shut down the instance before
+                creating the image. Image integrity may be affected.'''),
+            Arg('-b', '--block-device-mapping', metavar='DEVICE=MAPPED',
+                dest='BlockDeviceMapping', action='append',
+                type=ec2_block_device_mapping, default=[],
+                help='''define a block device mapping for the image, in the
+                form DEVICE=MAPPED, where "MAPPED" is "none", "ephemeral(0-3)",
+                or
+                "[SNAP_ID]:[SIZE]:[true|false]:[standard|VOLTYPE[:IOPS]]"''')]
 
     def print_result(self, result):
         print self.tabify(('IMAGE', result.get('imageId')))
diff --git a/euca2ools/commands/euca/createkeypair.py b/euca2ools/commands/euca/createkeypair.py
index 03b2172..04fbce1 100644
--- a/euca2ools/commands/euca/createkeypair.py
+++ b/euca2ools/commands/euca/createkeypair.py
@@ -28,13 +28,15 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 import os
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class CreateKeyPair(EucalyptusRequest):
     DESCRIPTION = 'Create a new SSH key pair for use with instances'
-    ARGS = [Arg('KeyName', metavar='KEYPAIR', help='name of the new key pair'),
+    ARGS = [Arg('KeyName', metavar='KEYPAIR',
+                help='name of the new key pair (required)'),
             Arg('-f', '--filename', metavar='FILE', route_to=None,
                 help='file name to save the private key to')]
 
diff --git a/euca2ools/commands/euca/createsecuritygroup.py b/euca2ools/commands/euca/createsecuritygroup.py
index cce7565..df4c73f 100644
--- a/euca2ools/commands/euca/createsecuritygroup.py
+++ b/euca2ools/commands/euca/createsecuritygroup.py
@@ -28,14 +28,18 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class CreateSecurityGroup(EucalyptusRequest):
     DESCRIPTION = 'Create a new security group'
-    ARGS = [Arg('-d', '--description', dest='GroupDescription', metavar='DESC',
-                required=True),
-            Arg('GroupName', metavar='GROUP', help='name of the new group')]
+    ARGS = [Arg('GroupName', metavar='GROUP',
+                help='name of the new group (required)'),
+            Arg('-d', '--description', dest='GroupDescription', metavar='DESC',
+                required=True, help='description of the new group (required)'),
+            Arg('-c', '--vpc', dest='VpcId', metavar='VPC',
+                help='[VPC only] ID of the VPC to create the group in')]
 
     def print_result(self, result):
         print self.tabify(('GROUP', result.get('groupId'),
diff --git a/euca2ools/commands/euca/createsnapshot.py b/euca2ools/commands/euca/createsnapshot.py
index b40d0fc..b418743 100644
--- a/euca2ools/commands/euca/createsnapshot.py
+++ b/euca2ools/commands/euca/createsnapshot.py
@@ -28,18 +28,20 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class CreateSnapshot(EucalyptusRequest):
     DESCRIPTION = 'Create a snapshot of a volume'
-    ARGS = [Arg('VolumeId', metavar='VOLUME', help='volume to snapshot'),
+    ARGS = [Arg('VolumeId', metavar='VOLUME',
+                help='volume to create a snapshot of (required)'),
             Arg('-d', '--description', metavar='DESC', dest='Description',
                 help='snapshot description')]
 
     def print_result(self, result):
-        print self.tabify(['SNAPSHOT',              result.get('snapshotId'),
-                           result.get('volumeId'),  result.get('status'),
+        print self.tabify(('SNAPSHOT', result.get('snapshotId'),
+                           result.get('volumeId'), result.get('status'),
                            result.get('startTime'), result.get('ownerId'),
                            result.get('volumeSize'),
-                           result.get('description')])
+                           result.get('description')))
diff --git a/euca2ools/commands/euca/createtags.py b/euca2ools/commands/euca/createtags.py
index 83d4681..7f28788 100644
--- a/euca2ools/commands/euca/createtags.py
+++ b/euca2ools/commands/euca/createtags.py
@@ -28,20 +28,20 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.argtypes import binary_tag_def
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
-from ..argtypes import binary_tag_def
+
 
 class CreateTags(EucalyptusRequest):
-    API_VERSION = '2010-08-31'
     DESCRIPTION = 'Add or overwrite tags for one or more resources'
     ARGS = [Arg('ResourceId', metavar='RESOURCE', nargs='+',
-                help='IDs of the resource(s) to tag'),
+                help='ID(s) of the resource(s) to tag (at least 1 required)'),
             Arg('--tag', dest='Tag', metavar='KEY[=VALUE]',
                 type=binary_tag_def, action='append', required=True,
                 help='''key and optional value of the tag to create, separated
-                        by an "=" character.  If no value is given the tag's
-                        value is set to an empty string.''')]
+                by an "=" character.  If no value is given the tag's value is
+                set to an empty string.  (at least 1 required)''')]
 
     def print_result(self, result):
         for resource_id in self.args['ResourceId']:
diff --git a/euca2ools/commands/euca/createvolume.py b/euca2ools/commands/euca/createvolume.py
index e1ec9da..3f39fdf 100644
--- a/euca2ools/commands/euca/createvolume.py
+++ b/euca2ools/commands/euca/createvolume.py
@@ -28,28 +28,37 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+from requestbuilder.exceptions import ArgumentError
+
 
 class CreateVolume(EucalyptusRequest):
     DESCRIPTION = 'Create a new volume'
-    ARGS = [Arg('-s', '--size', dest='Size', type=int,
-                help='''size of the new volume in GiB.  Required unless
-                        --snapshot is used'''),
+    ARGS = [Arg('-z', '--zone', dest='AvailabilityZone', metavar='ZONE',
+                required=True, help='''availability zone in which to create the
+                new volume (required)'''),
+            Arg('-s', '--size', dest='Size', type=int, help='''size of the new
+                volume in GiB (required unless --snapshot is used)'''),
             Arg('--snapshot', dest='SnapshotId', metavar='SNAPSHOT',
                 help='snapshot from which to create the new volume'),
-            Arg('-z', '--zone', dest='AvailabilityZone', metavar='ZONE',
-                required=True,
-                help='availability zone in which to create the new volume')]
+            Arg('-t', '--type', dest='VolumeType', metavar='VOLTYPE',
+                help='volume type'),
+            Arg('-i', '--iops', dest='Iops', type=int,
+                help='number of I/O operations per second')]
 
     def configure(self):
         EucalyptusRequest.configure(self)
         if not self.args.get('Size') and not self.args.get('SnapshotId'):
-            self._cli_parser.error('at least one of -s/--size and --snapshot '
-                                   'must be specified')
+            raise ArgumentError('-s/--size or --snapshot must be specified')
+        if self.args.get('Iops') and not self.args.get('VolumeType'):
+            raise ArgumentError('argument -i/--iops: -t/--type is required')
+        if self.args.get('Iops') and self.args.get('VolumeType') == 'standard':
+            raise ArgumentError(
+                'argument -i/--iops: not allowed with volume type "standard"')
 
     def print_result(self, result):
-        print self.tabify(['VOLUME', result.get('volumeId'),
+        print self.tabify(('VOLUME', result.get('volumeId'),
                            result.get('size'), result.get('snapshotId'),
                            result.get('availabilityZone'),
-                           result.get('status'), result.get('createTime')])
+                           result.get('status'), result.get('createTime')))
diff --git a/euca2ools/commands/euca/deletekeypair.py b/euca2ools/commands/euca/deletekeypair.py
index 4517bbf..dd9fac0 100644
--- a/euca2ools/commands/euca/deletekeypair.py
+++ b/euca2ools/commands/euca/deletekeypair.py
@@ -28,13 +28,14 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class DeleteKeyPair(EucalyptusRequest):
-    DESCRIPTION = 'Delete an existing keypair'
+    DESCRIPTION = 'Delete a key pair'
     ARGS = [Arg('KeyName', metavar='KEYPAIR',
-                help='name of the keypair to delete')]
+                help='name of the key pair to delete (required)')]
 
     def print_result(self, result):
-        print self.tabify(['KEYPAIR', self.args['KeyName']])
+        print self.tabify(('KEYPAIR', self.args['KeyName']))
diff --git a/euca2ools/commands/euca/deletesecuritygroup.py b/euca2ools/commands/euca/deletesecuritygroup.py
index 9ce27aa..300c54d 100644
--- a/euca2ools/commands/euca/deletesecuritygroup.py
+++ b/euca2ools/commands/euca/deletesecuritygroup.py
@@ -28,12 +28,20 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class DeleteSecurityGroup(EucalyptusRequest):
     DESCRIPTION = 'Delete a security group'
-    ARGS = [Arg('GroupName', metavar='GROUP')]
+    ARGS = [Arg('group', metavar='GROUP', route_to=None,
+                help='name or ID of the security group to delete (required)')]
+
+    def preprocess(self):
+        if self.args['group'].startswith('sg-'):
+            self.params['GroupId'] = self.args['group']
+        else:
+            self.params['GroupName'] = self.args['group']
 
     def print_result(self, result):
         print self.tabify(('RETURN', result.get('return')))
diff --git a/euca2ools/commands/euca/deletesnapshot.py b/euca2ools/commands/euca/deletesnapshot.py
index 99d0b2f..f0cf73a 100644
--- a/euca2ools/commands/euca/deletesnapshot.py
+++ b/euca2ools/commands/euca/deletesnapshot.py
@@ -28,12 +28,14 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class DeleteSnapshot(EucalyptusRequest):
     DESCRIPTION = 'Delete a snapshot'
-    ARGS = [Arg('SnapshotId', metavar='SNAPSHOT', help='snapshot to delete')]
+    ARGS = [Arg('SnapshotId', metavar='SNAPSHOT',
+                help='ID of the snapshot to delete (required)')]
 
     def print_result(self, result):
-        print self.tabify(['SNAPSHOT', self.args['SnapshotId']])
+        print self.tabify(('SNAPSHOT', self.args['SnapshotId']))
diff --git a/euca2ools/commands/euca/deletetags.py b/euca2ools/commands/euca/deletetags.py
index e2bcf00..34452f4 100644
--- a/euca2ools/commands/euca/deletetags.py
+++ b/euca2ools/commands/euca/deletetags.py
@@ -28,23 +28,22 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.argtypes import ternary_tag_def
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
-from ..argtypes import ternary_tag_def
+
 
 class DeleteTags(EucalyptusRequest):
-    API_VERSION = '2010-08-31'
     DESCRIPTION = 'Delete tags from one or more resources'
-    ARGS = [Arg('ResourceId', metavar='RESOURCE', nargs='+',
-                help='IDs of the resource(s) to un-tag'),
+    ARGS = [Arg('ResourceId', metavar='RESOURCE', nargs='+', help='''ID(s) of
+                the resource(s) to un-tag (at least 1 required)'''),
             Arg('--tag', dest='Tag', metavar='KEY[=[VALUE]]',
                 type=ternary_tag_def, action='append', required=True,
                 help='''key and optional value of the tag to delete, separated
-                        by an "=" character.  If no value is given, but a "="
-                        character is, then the tag is deleted if its value is
-                        not an empty string.  If neither a value nor a "="
-                        character is given then the tag with that key is
-                        deleted regardless of its value.''')]
-
-    def print_result(self, result):
-        pass
+                by an "=" character.  If you specify a value then the tag is
+                deleted only if its value matches the one you specified.  If
+                you specify the empty string as the value (e.g. "--tag foo=")
+                then the tag is deleted only if its value is the empty
+                string.  If you do not specify a value (e.g. "--tag foo") then
+                the tag is deleted regardless of its value. (at least 1
+                required)''')]
diff --git a/euca2ools/commands/euca/deletevolume.py b/euca2ools/commands/euca/deletevolume.py
index 2b7cbe1..c0a58c3 100644
--- a/euca2ools/commands/euca/deletevolume.py
+++ b/euca2ools/commands/euca/deletevolume.py
@@ -28,12 +28,14 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class DeleteVolume(EucalyptusRequest):
     DESCRIPTION = 'Delete a volume'
-    ARGS = [Arg('VolumeId', metavar='VOLUME', help='volume to delete')]
+    ARGS = [Arg('VolumeId', metavar='VOLUME',
+                help='ID of the volume to delete (required)')]
 
     def print_result(self, result):
-        print self.tabify(['VOLUME', self.args['VolumeId']])
+        print self.tabify(('VOLUME', self.args['VolumeId']))
diff --git a/euca2ools/commands/euca/deregisterimage.py b/euca2ools/commands/euca/deregisterimage.py
index c52a08e..a640e73 100644
--- a/euca2ools/commands/euca/deregisterimage.py
+++ b/euca2ools/commands/euca/deregisterimage.py
@@ -28,12 +28,17 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class DeregisterImage(EucalyptusRequest):
-    DESCRIPTION = 'De-register an image'
-    ARGS = [Arg('ImageId', metavar='IMAGE', help='image to de-register')]
+    DESCRIPTION = ('De-register an image.  After you de-register an image it '
+                   'cannot be used to launch new instances.\n\nNote that in '
+                   'Eucalyptus 3 you may need to run this twice to completely '
+                   "remove an image's registration from the system.")
+    ARGS = [Arg('ImageId', metavar='IMAGE',
+                help='ID of the image to de-register (required)')]
 
     def print_result(self, result):
-        print self.tabify(['IMAGE', self.args['ImageId']])
+        print self.tabify(('IMAGE', self.args['ImageId']))
diff --git a/euca2ools/commands/euca/describeaddresses.py b/euca2ools/commands/euca/describeaddresses.py
index b556dd9..5350884 100644
--- a/euca2ools/commands/euca/describeaddresses.py
+++ b/euca2ools/commands/euca/describeaddresses.py
@@ -28,21 +28,27 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg, Filter
-from . import EucalyptusRequest
+
 
 class DescribeAddresses(EucalyptusRequest):
-    API_VERSION = '2011-01-01'
     DESCRIPTION = 'Show information about elastic IP addresses'
     ARGS = [Arg('address', nargs='*', route_to=None,
-                help='''limit results to one or more elastic IP addresses or
-                        allocation IDs''')]
-    FILTERS = [Filter('allocation-id', help='allocation ID (VPC only)'),
-               Filter('association-id', help='association ID (VPC only)'),
-               Filter('domain', choices=['standard', 'vpc'],
+                help='''limit results to specific elastic IP addresses or
+                VPC allocation IDs''')]
+    FILTERS = [Filter('allocation-id', help='[VPC only] allocation ID'),
+               Filter('association-id', help='[VPC only] association ID'),
+               Filter('domain', choices=('standard', 'vpc'),
                       help='whether the address is a standard or VPC address'),
                Filter('instance-id',
                       help='instance the address is associated with'),
+               Filter('network-interface-id', help='''[VPC only] network
+                      interface the address is associated with'''),
+               Filter('network-interface-owner-id', help='''[VPC only] ID of
+                      the network interface's owner'''),
+               Filter('private-ip-address', help='''[VPC only] private address
+                      associated with the public address'''),
                Filter('public-ip', help='the elastic IP address')]
     LIST_TAGS = ['addressesSet']
 
@@ -52,14 +58,16 @@ class DescribeAddresses(EucalyptusRequest):
         public_ips = set(self.args.get('address', [])) - alloc_ids
         self.params = {}
         if alloc_ids:
-            self.params['AllocationId'] = list(alloc_ids)
+            self.params['AllocationId'] = list(sorted(alloc_ids))
         if public_ips:
-            self.params['PublicIp'] = list(public_ips)
+            self.params['PublicIp'] = list(sorted(public_ips))
 
     def print_result(self, result):
         for addr in result.get('addressesSet', []):
-            print self.tabify(['ADDRESS', addr.get('publicIp'),
+            print self.tabify(('ADDRESS', addr.get('publicIp'),
                                addr.get('instanceId'),
                                addr.get('domain', 'standard'),
                                addr.get('allocationId'),
-                               addr.get('associationId')])
+                               addr.get('associationId'),
+                               addr.get('networkInterfaceId'),
+                               addr.get('privateIpAddress')))
diff --git a/euca2ools/commands/euca/describeavailabilityzones.py b/euca2ools/commands/euca/describeavailabilityzones.py
index e699b8c..710a74c 100644
--- a/euca2ools/commands/euca/describeavailabilityzones.py
+++ b/euca2ools/commands/euca/describeavailabilityzones.py
@@ -28,17 +28,17 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 import euca2ools.utils
 from requestbuilder import Arg, Filter
-from . import EucalyptusRequest
+
 
 class DescribeAvailabilityZones(EucalyptusRequest):
-    DESCRIPTION = 'Display availability zones within the active region'
-    API_VERSION = '2010-08-31'
+    DESCRIPTION = 'Display availability zones within the current region'
     ARGS = [Arg('ZoneName', metavar='ZONE', nargs='*',
-                help='limit results to one or more availability zones')]
-    FILTERS = [Filter('message', help=('message giving information about the'
-                      'availability zone')),
+                help='limit results to specific availability zones')]
+    FILTERS = [Filter('message', help='''message giving information about the
+                      'availability zone'''),
                Filter('region-name',
                       help='region the availability zone is in'),
                Filter('state', help='state of the availability zone'),
diff --git a/euca2ools/commands/euca/describebundletasks.py b/euca2ools/commands/euca/describebundletasks.py
index e75cee0..ab67607 100644
--- a/euca2ools/commands/euca/describebundletasks.py
+++ b/euca2ools/commands/euca/describebundletasks.py
@@ -28,14 +28,14 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg, Filter
-from . import EucalyptusRequest
+
 
 class DescribeBundleTasks(EucalyptusRequest):
     DESCRIPTION = 'Describe current instance-bundling tasks'
-    API_VERSION = '2010-08-31'
     ARGS = [Arg('BundleId', metavar='BUNDLE', nargs='*',
-                help='limit results to one or more bundle tasks')]
+                help='limit results to specific bundle tasks')]
     FILTERS = [Filter('bundle-id', help='bundle task ID'),
                Filter('error-code',
                       help='if the task failed, the error code returned'),
diff --git a/euca2ools/commands/euca/describeimageattribute.py b/euca2ools/commands/euca/describeimageattribute.py
index dae3b68..02469ad 100644
--- a/euca2ools/commands/euca/describeimageattribute.py
+++ b/euca2ools/commands/euca/describeimageattribute.py
@@ -28,8 +28,9 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg, MutuallyExclusiveArgList
-from . import EucalyptusRequest
+
 
 class DescribeImageAttribute(EucalyptusRequest):
     DESCRIPTION = 'Show information about an attribute of an image'
diff --git a/euca2ools/commands/euca/describeimages.py b/euca2ools/commands/euca/describeimages.py
index e0db1be..e1debd3 100644
--- a/euca2ools/commands/euca/describeimages.py
+++ b/euca2ools/commands/euca/describeimages.py
@@ -28,19 +28,17 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg, Filter, GenericTagFilter
-from . import EucalyptusRequest
+from requestbuilder.exceptions import ArgumentError
 
-class DescribeImages(EucalyptusRequest):
-    DESCRIPTION = '''\
-        Show information about images
-
-        By default, only images the caller owns and images for which the caller
-        has explicit launch permissions are shown.'''
 
-    API_VERSION = '2010-08-31'
+class DescribeImages(EucalyptusRequest):
+    DESCRIPTION = ('Show information about images\n\nBy default, only images '
+                   'your account owns and images for which your account has '
+                   'explicit launch permissions are shown.')
     ARGS = [Arg('ImageId', metavar='IMAGE', nargs='*',
-                help='limit results to one or more images'),
+                help='limit results to specific images'),
             Arg('-a', '--all', action='store_true', route_to=None,
                 help='describe all images'),
             Arg('-o', '--owner', dest='Owner', metavar='ACCOUNT',
@@ -48,20 +46,23 @@ class DescribeImages(EucalyptusRequest):
                 help='describe images owned by the specified owner'),
             Arg('-x', '--executable-by', dest='ExecutableBy',
                 metavar='ACCOUNT', action='append',
-                help='''describe images for which the specified entity has
-                        explicit launch permissions''')]
+                help='''describe images for which the specified account has
+                explicit launch permissions''')]
     FILTERS = [Filter('architecture', choices=('i386', 'x86_64', 'armhf'),
-                      help='image architecture'),
+                      help='CPU architecture'),
                Filter('block-device-mapping.delete-on-termination',
                       help='''whether a volume is deleted upon instance
-                              termination'''),
+                      termination'''),
                Filter('block-device-mapping.device-name',
                       help='device name for a volume mapped to the image'),
                Filter('block-device-mapping.snapshot-id',
                       help='snapshot ID for a volume mapped to the image'),
                Filter('block-device-mapping.volume-size',
                       help='volume size for a volume mapped to the image'),
+               Filter('block-device-mapping.volume-type',
+                      help='volume type for a volume mapped to the image'),
                Filter('description', help='image description'),
+               Filter('hypervisor', help='image\'s hypervisor type'),
                Filter('image-id'),
                Filter('image-type', choices=('machine', 'kernel', 'ramdisk'),
                       help='image type ("machine", "kernel", or "ramdisk")'),
@@ -82,7 +83,7 @@ class DescribeImages(EucalyptusRequest):
                       help='root device type ("ebs" or "instance-store")'),
                Filter('state', choices=('available', 'pending', 'failed'),
                       help='''image state ("available", "pending", or
-                              "failed")'''),
+                      "failed")'''),
                Filter('state-reason-code',
                       help='reason code for the most recent state change'),
                Filter('state-reason-message',
@@ -93,34 +94,33 @@ class DescribeImages(EucalyptusRequest):
                GenericTagFilter('tag:KEY',
                                 help='specific tag key/value combination'),
                Filter('virtualization-type', choices=('paravirtual', 'hvm'),
-                      help='virtualization type ("paravirtual" or "hvm")'),
-               Filter('hypervisor', choices=('ovm', 'xen'),
-                      help='image\'s hypervisor type ("ovm" or "xen")')]
-    LIST_TAGS = ['imagesSet', 'blockDeviceMapping', 'tagSet']
+                      help='virtualization type ("paravirtual" or "hvm")')]
+    LIST_TAGS = ['imagesSet', 'productCodes', 'blockDeviceMapping', 'tagSet']
 
     def configure(self):
         EucalyptusRequest.configure(self)
         if self.args['all']:
             if self.args.get('ImageId'):
-                self._cli_parser.error('argument -a/--all: not allowed with '
-                                       'a list of images')
+                raise ArgumentError('argument -a/--all: not allowed with '
+                                    'a list of images')
             if self.args.get('ExecutableBy'):
-                self._cli_parser.error('argument -a/--all: not allowed with '
-                                       'argument -x/--executable-by')
+                raise ArgumentError('argument -a/--all: not allowed with '
+                                    'argument -x/--executable-by')
             if self.args.get('Owner'):
-                self._cli_parser.error('argument -a/--all: not allowed with '
-                                       'argument -o/--owner')
+                raise ArgumentError('argument -a/--all: not allowed with '
+                                    'argument -o/--owner')
 
     def main(self):
         if not any(self.args.get(item) for item in ('all', 'ImageId',
                                                     'ExecutableBy', 'Owner')):
             # Default to owned images and images with explicit launch perms
-            self.params = {'Owner': 'self'}
+            self.params['Owner'] = ['self']
             owned = self.send()
-            self.params = {'ExecutableBy': 'self'}
+            del self.params['Owner']
+            self.params['ExecutableBy'] = ['self']
             executable = self.send()
-            self.params = None
-            owned['imagesSet'] = (owned.get(     'imagesSet', []) +
+            del self.params['ExecutableBy']
+            owned['imagesSet'] = (owned.get('imagesSet', []) +
                                   executable.get('imagesSet', []))
             return owned
         else:
diff --git a/euca2ools/commands/euca/describeinstances.py b/euca2ools/commands/euca/describeinstances.py
index 52c4a1e..97311d3 100644
--- a/euca2ools/commands/euca/describeinstances.py
+++ b/euca2ools/commands/euca/describeinstances.py
@@ -28,22 +28,33 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg, Filter, GenericTagFilter
-from . import EucalyptusRequest
+
 
 class DescribeInstances(EucalyptusRequest):
-    API_VERSION = '2010-08-31'
     DESCRIPTION = 'Show information about instances'
     ARGS = [Arg('InstanceId', metavar='INSTANCE', nargs='*',
-                help='Limit results to one or more instances')]
+                help='limit results to specific instances')]
     FILTERS = [Filter('architecture', choices=('i386', 'x86_64', 'armhf'),
                       help='CPU architecture'),
+               Filter('association.allocation-id',
+                      help='''[VPC only] allocation ID bound to a network
+                      interface's elastic IP address'''),
+               Filter('association.association-id', help='''[VPC only]
+                      association ID returned when an elastic IP was associated
+                      with a network interface'''),
+               Filter('association.ip-owner-id',
+                      help='''[VPC only] ID of the owner of the elastic IP
+                      address associated with a network interface'''),
+               Filter('association.public-ip', help='''[VPC only] address of
+                      the elastic IP address bound to a network interface'''),
                Filter('availability-zone'),
                Filter('block-device-mapping.attach-time',
                       help='volume attachment time'),
                Filter('block-device-mapping.delete-on-termination', type=bool,
                       help='''whether a volume is deleted upon instance
-                              termination'''),
+                      termination'''),
                Filter('block-device-mapping.device-name',
                       help='volume device name (e.g. /dev/sdf)'),
                Filter('block-device-mapping.status', help='volume status'),
@@ -51,56 +62,133 @@ class DescribeInstances(EucalyptusRequest):
                Filter('client-token',
                       help='idempotency token provided at instance run time'),
                Filter('dns-name', help='public DNS name'),
-               Filter('group-id', help='security group membership'),
+               # EC2's documentation for "group-id" refers VPC users to
+               # "instance.group-id", while their documentation for the latter
+               # refers them to the former.  Consequently, I'm not going to
+               # document a difference for either.  They both seem to work for
+               # non-VPC instances.
+               Filter('group-id', help='security group ID'),
+               Filter('group-name', help='security group name'),
                Filter('hypervisor', help='hypervisor type'),
                Filter('image-id', help='machine image ID'),
+               Filter('instance.group-id', help='security group ID'),
+               Filter('instance.group-name', help='security group name'),
                Filter('instance-id'),
-               Filter('instance-lifecycle', choices=['spot'],
+               Filter('instance-lifecycle', choices=('spot',),
                       help='whether this is a spot instance'),
                Filter('instance-state-code', type=int,
                       help='numeric code identifying instance state'),
                Filter('instance-state-name', help='instance state'),
-               Filter('instance-type',),
+               Filter('instance-type'),
                Filter('ip-address', help='public IP address'),
                Filter('kernel-id', help='kernel image ID'),
                Filter('key-name',
                       help='key pair name provided at instance launch time'),
                Filter('launch-index', help='launch index within a reservation'),
                Filter('launch-time', help='instance launch time'),
-               Filter('monitoring-state', help='whether monitoring is enabled'),
-               Filter('owner-id', help='instance owner\'s account ID'),
+               Filter('monitoring-state', choices=('enabled', 'disabled'),
+                      help='monitoring state ("enabled" or "disabled")'),
+               Filter('network-interface.addresses.association.ip-owner-id',
+                      help='''[VPC only] ID of the owner of the private IP
+                      address associated with a network interface'''),
+               Filter('network-interface.addresses.association.public-ip',
+                      help='''[VPC only] ID of the association of an elastic IP
+                      address with a network interface'''),
+               Filter('network-interface.addresses.primary',
+                      choices=('true', 'false'),
+                      help='''[VPC only] whether the IP address of the VPC
+                      network interface is the primary private IP address'''),
+               Filter('network-interface.addresses.private-ip-address',
+                      help='''[VPC only] network interface's private IP
+                      address'''),
+               Filter('network-interface.attachment.device-index', type=int,
+                      help='''[VPC only] device index to which a network
+                      interface is attached'''),
+               Filter('network-interface.attachment.attach-time',
+                      help='''[VPC only] time a network interface was attached
+                      to an instance'''),
+               Filter('network-interface.attachment.attachment-id',
+                      help='''[VPC only] ID of a network interface's
+                      attachment'''),
+               Filter('network-interface.attachment.delete-on-termination',
+                      choices=('true', 'false'),
+                      help='''[VPC only] whether a network interface attachment
+                      is deleted when an instance is terminated'''),
+               Filter('network-interface.attachment.instance-owner-id',
+                      help='''[VPC only] ID of the instance to which a network
+                      interface is attached'''),
+               Filter('network-interface.attachment.status',
+                      choices=('attaching', 'attached', 'detaching',
+                               'detached'),
+                      help="[VPC only] network interface's attachment status"),
+               Filter('network-interface.availability-zone',
+                      help="[VPC only] network interface's availability zone"),
+               Filter('network-interface.description',
+                      help='[VPC only] description of a network interface'),
+               Filter('network-interface.group-id',
+                      help="[VPC only] network interface's security group ID"),
+               Filter('network-interface.group-name', help='''[VPC only]
+                      network interface's security group name'''),
+               Filter('network-interface.mac-address',
+                      help="[VPC only] network interface's hardware address"),
+               Filter('network-interface.network-interface.id',
+                      help='[VPC only] ID of a network interface'),
+               Filter('network-interface.owner-id',
+                      help="[VPC only] ID of a network interface's owner"),
+               Filter('network-interface.private-dns-name',
+                      help="[VPC only] network interface's private DNS name"),
+               Filter('network-interface.requester-id',
+                      help="[VPC only] network interface's requester ID"),
+               Filter('network-interface.requester-managed',
+                      help='''[VPC only] whether the network interface is
+                      managed by the service'''),
+               Filter('network-interface.source-destination-check',
+                      choices=('true', 'false'),
+                      help='''[VPC only] whether source/destination checking is
+                      enabled for a network interface'''),
+               Filter('network-interface.status',
+                      help="[VPC only] network interface's status"),
+               Filter('network-interface.subnet-id',
+                      help="[VPC only] ID of a network interface's subnet"),
+               Filter('network-interface.vpc-id',
+                      help="[VPC only] ID of a network interface's VPC"),
+               Filter('owner-id', help="instance owner's account ID"),
                Filter('placement-group-name'),
-               Filter('platform', choices=['windows'],
-                      help='whether this is a Windows instance'),
+               Filter('platform', help='"windows" for Windows instances'),
                Filter('private-dns-name'),
                Filter('private-ip-address'),
                Filter('product-code'),
+               Filter('product-code.type', choices=('devpay', 'marketplace'),
+                      help='type of product code ("devpay" or "marketplace")'),
                Filter('ramdisk-id', help='ramdisk image ID'),
-               Filter('reason', help='reason for the more recent state change'),
+               Filter('reason',
+                      help="reason for the instance's current state"),
                Filter('requestor-id',
                       help='ID of the entity that launched an instance'),
                Filter('reservation-id'),
                Filter('root-device-name',
                       help='root device name (e.g. /dev/sda1)'),
-               Filter('root-device-type', choices=['ebs', 'instance-store'],
-                      help='root device type (ebs or instance-store)'),
+               Filter('root-device-type', choices=('ebs', 'instance-store'),
+                      help='root device type ("ebs" or "instance-store")'),
                Filter('spot-instance-request-id'),
                Filter('state-reason-code',
                       help='reason code for the most recent state change'),
                Filter('state-reason-message',
-                      help='message for the most recent state change'),
+                      help='message describing the most recent state change'),
                Filter('subnet-id',
-                      help='ID of the VPC subnet the instance is in'),
+                      help='[VPC only] ID of the subnet the instance is in'),
                Filter('tag-key',
                       help='name of any tag assigned to the instance'),
                Filter('tag-value',
                       help='value of any tag assigned to the instance'),
                GenericTagFilter('tag:KEY',
                                 help='specific tag key/value combination'),
-               Filter('virtualization-type', choices=['paravirtual', 'hvm']),
-               Filter('vpc-id', help='ID of the VPC the instance is in')]
+               Filter('virtualization-type', choices=('paravirtual', 'hvm')),
+               Filter('vpc-id',
+                      help='[VPC only] ID of the VPC the instance is in')]
     LIST_TAGS = ['reservationSet', 'instancesSet', 'groupSet', 'tagSet',
-                 'blockDeviceMapping', 'productCodes']
+                 'blockDeviceMapping', 'productCodes', 'networkInterfaceSet',
+                 'attachment', 'association', 'privateIpAddressesSet']
 
     def print_result(self, result):
         for reservation in result.get('reservationSet'):
diff --git a/euca2ools/commands/euca/describekeypairs.py b/euca2ools/commands/euca/describekeypairs.py
index 35e93a5..47fc683 100644
--- a/euca2ools/commands/euca/describekeypairs.py
+++ b/euca2ools/commands/euca/describekeypairs.py
@@ -28,11 +28,11 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg, Filter
-from . import EucalyptusRequest
+
 
 class DescribeKeyPairs(EucalyptusRequest):
-    API_VERSION = '2010-08-31'
     DESCRIPTION = 'Display information about available key pairs'
     ARGS = [Arg('KeyName', nargs='*', metavar='KEYPAIR',
                 help='limit results to specific key pairs')]
diff --git a/euca2ools/commands/euca/describeregions.py b/euca2ools/commands/euca/describeregions.py
index e5ddaf9..cd1b73b 100644
--- a/euca2ools/commands/euca/describeregions.py
+++ b/euca2ools/commands/euca/describeregions.py
@@ -28,11 +28,11 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg, Filter
-from . import EucalyptusRequest
+
 
 class DescribeRegions(EucalyptusRequest):
-    API_VERSION = '2010-08-31'
     DESCRIPTION = 'Display information about regions'
     ARGS = [Arg('RegionName', nargs='*', metavar='REGION',
                 help='limit results to specific regions')]
diff --git a/euca2ools/commands/euca/describesecuritygroups.py b/euca2ools/commands/euca/describesecuritygroups.py
index d2251dc..fa554ff 100644
--- a/euca2ools/commands/euca/describesecuritygroups.py
+++ b/euca2ools/commands/euca/describesecuritygroups.py
@@ -28,20 +28,17 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg, Filter
-from . import EucalyptusRequest
 
-class DescribeSecurityGroups(EucalyptusRequest):
-    DESCRIPTION = '''\
-        Show information about security groups
-
-        Note that filters are matched on literal strings only, so
-        "--filter ip-permission.from-port=22" will *not* match a group with a
-        port range of 20 to 30.'''
 
-    API_VERSION = '2012-12-01'
-    ARGS = [Arg('group', metavar='GROUP', nargs='*', route_to=None, default=[],
-                help='limit results to one or more security groups')]
+class DescribeSecurityGroups(EucalyptusRequest):
+    DESCRIPTION = ('Show information about security groups\n\nNote that '
+                   'filters are matched on literal strings only, so '
+                   '"--filter ip-permission.from-port=22" will *not* match a '
+                   'group with a port range of 20 to 30.')
+    ARGS = [Arg('group', metavar='GROUP', nargs='*', route_to=None,
+                default=[], help='limit results to specific security groups')]
     FILTERS = [Filter('description', help='group description'),
                Filter('group-id'),
                Filter('group-name'),
@@ -61,9 +58,11 @@ class DescribeSecurityGroups(EucalyptusRequest):
                Filter('owner-id', help=="account ID of the group's owner"),
                Filter('tag-key', help='key of a tag assigned to the group'),
                Filter('tag-value',
-                      help='value of a tag assigned to the group')]
+                      help='value of a tag assigned to the group'),
+               Filter('vpc-id',
+                      help='[VPC only] ID of a VPC the group belongs to')]
     LIST_TAGS = ['securityGroupInfo', 'ipPermissions', 'ipPermissionsEgress',
-                 'groups', 'ipRanges']
+                 'groups', 'ipRanges', 'tagSet']
 
     def preprocess(self):
         for group in self.args['group']:
@@ -81,12 +80,13 @@ class DescribeSecurityGroups(EucalyptusRequest):
     def print_group(self, group):
         print self.tabify(('GROUP', group.get('groupId'), group.get('ownerId'),
                            group.get('groupName'),
-                           group.get('groupDescription')))
+                           group.get('groupDescription'),
+                           group.get('vpcId')))
         for perm in group.get('ipPermissions', []):
             perm_base = ['PERMISSION', group.get('ownerId'),
-                         group.get('groupName'), 'ALLOWS']
-            perm_base.extend([perm.get('ipProtocol'), perm.get('fromPort'),
-                              perm.get('toPort')])
+                         group.get('groupName'), 'ALLOWS',
+                         perm.get('ipProtocol'), perm.get('fromPort'),
+                         perm.get('toPort')]
             for cidr_range in perm.get('ipRanges', []):
                 perm_item = ['FROM', 'CIDR', cidr_range.get('cidrIp'),
                              'ingress']
@@ -101,9 +101,9 @@ class DescribeSecurityGroups(EucalyptusRequest):
                 print self.tabify(perm_base + perm_item)
         for perm in group.get('ipPermissionsEgress', []):
             perm_base = ['PERMISSION', group.get('ownerId'),
-                         group.get('groupName'), 'ALLOWS']
-            perm_base.extend([perm.get('ipProtocol'), perm.get('fromPort'),
-                              perm.get('toPort')])
+                         group.get('groupName'), 'ALLOWS',
+                         perm.get('ipProtocol'), perm.get('fromPort'),
+                         perm.get('toPort')]
             for cidr_range in perm.get('ipRanges', []):
                 perm_item = ['TO', 'CIDR', cidr_range.get('cidrIp'), 'egress']
                 print self.tabify(perm_base + perm_item)
@@ -115,3 +115,6 @@ class DescribeSecurityGroups(EucalyptusRequest):
                     perm_item.extend(['GRPNAME', othergroup['groupName']])
                 perm_item.append('egress')
                 print self.tabify(perm_base + perm_item)
+        for tag in group.get('tagSet', []):
+            self.print_resource_tag(tag, (group.get('groupId') or
+                                          group.get('groupName')))
diff --git a/euca2ools/commands/euca/describesnapshots.py b/euca2ools/commands/euca/describesnapshots.py
index b06da4c..0eb978c 100644
--- a/euca2ools/commands/euca/describesnapshots.py
+++ b/euca2ools/commands/euca/describesnapshots.py
@@ -29,16 +29,15 @@
 # POSSIBILITY OF SUCH DAMAGE.
 
 from argparse import SUPPRESS
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg, Filter, GenericTagFilter
-from . import EucalyptusRequest
+from requestbuilder.exceptions import ArgumentError
 
-class DescribeSnapshots(EucalyptusRequest):
-    API_VERSION = '2010-08-31'
-    DESCRIPTION = '''\
-        Show information about snapshots
 
-        By default, only snapshots explicitly restorable by the caller are
-        shown.'''
+class DescribeSnapshots(EucalyptusRequest):
+    DESCRIPTION = ('Show information about snapshots\n\nBy default, only '
+                   'snapshots your account owns and snapshots for which your '
+                   'account has explicit restore permissions are shown.')
     ARGS = [Arg('SnapshotId', nargs='*', metavar='SNAPSHOT',
                 help='limit results to specific snapshots'),
             Arg('-a', '--all', action='store_true', route_to=None,
@@ -67,17 +66,29 @@ class DescribeSnapshots(EucalyptusRequest):
 
     def configure(self):
         EucalyptusRequest.configure(self)
-        if not any(self.args.get(item) for item in ('all', 'Owner',
-                                                    'RestorableBy')):
-            # Default to restorable snapshots
-            self.args['RestorableBy'] = ['self']
-        elif self.args.get('all'):
+        if self.args.get('all'):
             if self.args.get('Owner'):
-                self._cli_parser.error('argument -a/--all: not allowed with '
-                                       'argument -o/--owner')
+                raise ArgumentError('argument -a/--all: not allowed with '
+                                    'argument -o/--owner')
             if self.args.get('RestorableBy'):
-                self._cli_parser.error('argument -a/--all: not allowed with '
-                                       'argument -r/--restorable-by')
+                raise ArgumentError('argument -a/--all: not allowed with '
+                                    'argument -r/--restorable-by')
+
+    def main(self):
+        if not any(self.args.get(item) for item in ('all', 'Owner',
+                                                    'RestorableBy')):
+            # Default to owned snapshots and those with explicit restore perms
+            self.params['Owner'] = ['self']
+            owned = self.send()
+            del self.params['Owner']
+            self.params['RestorableBy'] = ['self']
+            restorable = self.send()
+            del self.params['RestorableBy']
+            owned['snapshotSet'] = (owned.get('snapshotSet', []) +
+                                    restorable.get('snapshotSet', []))
+            return owned
+        else:
+            return self.send()
 
     def print_result(self, result):
         for snapshot in result.get('snapshotSet', []):
diff --git a/euca2ools/commands/euca/describetags.py b/euca2ools/commands/euca/describetags.py
index a104dd9..ab2ab6d 100644
--- a/euca2ools/commands/euca/describetags.py
+++ b/euca2ools/commands/euca/describetags.py
@@ -28,12 +28,12 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest, RESOURCE_TYPE_MAP
 from requestbuilder import Filter
-from . import EucalyptusRequest, RESOURCE_TYPE_MAP
+
 
 class DescribeTags(EucalyptusRequest):
-    API_VERSION = '2010-08-31'
-    DESCRIPTION = 'List tags associated with your account'
+    DESCRIPTION = "List tags associated with your account's resources"
     FILTERS = [Filter('key'),
                Filter('resource-id'),
                Filter('resource-type',
diff --git a/euca2ools/commands/euca/describevolumes.py b/euca2ools/commands/euca/describevolumes.py
index 52de269..1094121 100644
--- a/euca2ools/commands/euca/describevolumes.py
+++ b/euca2ools/commands/euca/describevolumes.py
@@ -28,14 +28,14 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg, Filter, GenericTagFilter
-from . import EucalyptusRequest
+
 
 class DescribeVolumes(EucalyptusRequest):
     DESCRIPTION = 'Display information about volumes'
-    API_VERSION = '2010-08-31'
     ARGS = [Arg('VolumeId', metavar='VOLUME', nargs='*',
-                help='volume(s) to describe (default: all volumes)')]
+                help='limit results to specific volumes')]
     FILTERS = [Filter('attachment.attach-time', help='attachment start time'),
                Filter('attachment.delete-on-termination', help='''whether the
                       volume will be deleted upon instance termination'''),
@@ -44,21 +44,22 @@ class DescribeVolumes(EucalyptusRequest):
                Filter('attachment.instance-id',
                       help='ID of the instance the volume is attached to'),
                Filter('attachment.status', help='attachment state',
-                      choices=['attaching', 'attached', 'detaching',
-                               'detached']),
+                      choices=('attaching', 'attached', 'detaching',
+                               'detached')),
                Filter('availability-zone'),
                Filter('create-time', help='creation time'),
                Filter('size', type=int, help='size in GiB'),
                Filter('snapshot-id',
                       help='snapshot from which the volume was created'),
-               Filter('status', choices=['creating', 'available', 'in-use',
-                                         'deleting', 'deleted', 'error']),
+               Filter('status', choices=('creating', 'available', 'in-use',
+                                         'deleting', 'deleted', 'error')),
                Filter('tag-key', help='key of a tag assigned to the volume'),
                Filter('tag-value',
                       help='value of a tag assigned to the volume'),
                GenericTagFilter('tag:KEY',
                                 help='specific tag key/value combination'),
-               Filter(name='volume-id')]
+               Filter(name='volume-id'),
+               Filter(name='volume-type')]
     LIST_TAGS = ['volumeSet', 'attachmentSet', 'tagSet']
 
     def print_result(self, result):
diff --git a/euca2ools/commands/euca/detachvolume.py b/euca2ools/commands/euca/detachvolume.py
index 909b6b8..8ed2a19 100644
--- a/euca2ools/commands/euca/detachvolume.py
+++ b/euca2ools/commands/euca/detachvolume.py
@@ -28,18 +28,20 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class DetachVolume(EucalyptusRequest):
     DESCRIPTION = 'Detach a volume from an instance'
-    ARGS = [Arg('VolumeId', metavar='VOLUME', help='volume to detach'),
+    ARGS = [Arg('VolumeId', metavar='VOLUME',
+                help='ID of the volume to detach (required)'),
             Arg('-i', '--instance', dest='InstanceID', metavar='INSTANCE',
                 help='instance to detach from'),
             Arg('-d', '--device', dest='Device', help='device name'),
             Arg('-f', '--force', action='store_const', const='true',
                 help='''detach without waiting for the instance.  Data may be
-                        lost''')]
+                lost.''')]
 
     def print_result(self, result):
         self.print_attachment(result)
diff --git a/euca2ools/commands/euca/disassociateaddress.py b/euca2ools/commands/euca/disassociateaddress.py
index ff65c10..88290a2 100644
--- a/euca2ools/commands/euca/disassociateaddress.py
+++ b/euca2ools/commands/euca/disassociateaddress.py
@@ -28,19 +28,32 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+from requestbuilder.exceptions import ArgumentError
+
 
 class DisassociateAddress(EucalyptusRequest):
     DESCRIPTION = 'Disassociate an elastic IP address from an instance'
-    ARGS = [Arg('address', route_to=None,
-                help='elastic IP address or association ID to disassociate')]
+    ARGS = [Arg('PublicIp', metavar='ADDRESS', nargs='?', help='''[Non-VPC
+                only] elastic IP address to disassociate (required)'''),
+            Arg('-a', '--association-id', dest='AssociationId',
+                metavar='ASSOC',
+                help="[VPC only] address's association ID (required)")]
 
-    def preprocess(self):
-        if self.args['address'].startswith('eipassoc'):
-            self.params = {'AssociationId': self.args['address']}
-        else:
-            self.params = {'PublicIp':      self.args['address']}
+    def configure(self):
+        EucalyptusRequest.configure(self)
+        if self.args.get('PublicIp'):
+            if self.args.get('AssociationId'):
+                raise ArgumentError('argument -a/--association-id: not '
+                                    'allowed with an IP address')
+            elif self.args['PublicIp'].startswith('eipassoc'):
+                raise ArgumentError('VPC elastic IP association IDs must be '
+                                    'be specified with -a/--association-id')
+        elif not self.args.get('AssociationId'):
+            raise ArgumentError(
+                'argument -a/--association-id or an IP address is required')
 
     def print_result(self, result):
-        print self.tabify(['ADDRESS', self.args['address']])
+        target = self.args.get('PublicIp') or self.args.get('AssociationId')
+        print self.tabify(('ADDRESS', target))
diff --git a/euca2ools/commands/euca/getconsoleoutput.py b/euca2ools/commands/euca/getconsoleoutput.py
index 541be6b..9c34e77 100644
--- a/euca2ools/commands/euca/getconsoleoutput.py
+++ b/euca2ools/commands/euca/getconsoleoutput.py
@@ -29,8 +29,9 @@
 # POSSIBILITY OF SUCH DAMAGE.
 
 import base64
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 CHAR_ESCAPES = {
         u'\x00': u'^@',    u'\x0c': u'^L',    u'\x17': u'^W',
@@ -45,11 +46,13 @@ CHAR_ESCAPES = {
         u'\x0b': u'^K',    u'\x16': u'^V',    u'\x7f': u'^?',
 }
 
+
 class GetConsoleOutput(EucalyptusRequest):
     DESCRIPTION = 'Retrieve console output for the specified instance'
-    ARGS = [Arg('InstanceId', metavar='INSTANCE',
-                help='instance to obtain console output from'),
-            Arg('--raw', action='store_true', route_to=None,
+    ARGS = [Arg('InstanceId', metavar='INSTANCE', help='''ID of the instance to
+                obtain console output from (required)'''),
+            Arg('-r', '--raw-console-output', action='store_true',
+                route_to=None,
                 help='Display raw output without escaping control characters')]
 
     def print_result(self, result):
@@ -57,7 +60,7 @@ class GetConsoleOutput(EucalyptusRequest):
         print result.get('timestamp', '')
         output = base64.b64decode(result.get('output', ''))
         output = output.decode()
-        if not self.args['raw']:
+        if not self.args['raw_console_output']:
             # Escape control characters
             for char, escape in CHAR_ESCAPES.iteritems():
                 output = output.replace(char, escape)
diff --git a/euca2ools/commands/euca/getpassword.py b/euca2ools/commands/euca/getpassword.py
index 4e863ef..7171cec 100644
--- a/euca2ools/commands/euca/getpassword.py
+++ b/euca2ools/commands/euca/getpassword.py
@@ -29,19 +29,19 @@
 # POSSIBILITY OF SUCH DAMAGE.
 
 import base64
+from euca2ools.commands.argtypes import file_contents
+from euca2ools.commands.euca.getpassworddata import GetPasswordData
 from M2Crypto import RSA
 from requestbuilder import Arg
-from ..argtypes import file_contents
-from .getpassworddata import GetPasswordData
+
 
 class GetPassword(GetPasswordData):
-    ACTION = 'GetPasswordData'
-    DESCRIPTION = '''Retrieve the administrator password for an instance
-                     running Windows'''
-    ARGS = [Arg('-k', '--priv-launch-key', metavar='PRIVKEY',
+    DESCRIPTION = ('Retrieve the administrator password for an instance '
+                   'running Windows')
+    ARGS = [Arg('-k', '--priv-launch-key', metavar='FILE',
                 type=file_contents, required=True, route_to=None,
                 help='''file containing the private key corresponding to the
-                        key pair supplied at instance launch time''')]
+                key pair supplied at instance launch time (required)''')]
 
     def print_result(self, result):
         try:
diff --git a/euca2ools/commands/euca/getpassworddata.py b/euca2ools/commands/euca/getpassworddata.py
index ae60438..b847171 100644
--- a/euca2ools/commands/euca/getpassworddata.py
+++ b/euca2ools/commands/euca/getpassworddata.py
@@ -28,14 +28,17 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class GetPasswordData(EucalyptusRequest):
-    DESCRIPTION = '''Retrieve the encrypted administrator password for an
-                     instance running Windows'''
-    ARGS = [Arg('InstanceId', metavar='INSTANCE',
-                help='instance to obtain the initial password for')]
+    DESCRIPTION = ('Retrieve the encrypted administrator password for an '
+                   'instance running Windows.  The encrypted password may be '
+                   'decrypted using the private key of the key pair given '
+                   'when launching the instance.')
+    ARGS = [Arg('InstanceId', metavar='INSTANCE', help='''ID of the instance to
+                obtain the initial password for (required)''')]
 
     def print_result(self, result):
         if result.get('passwordData'):
diff --git a/euca2ools/commands/euca/importkeypair.py b/euca2ools/commands/euca/importkeypair.py
index faa5299..09a1dd5 100644
--- a/euca2ools/commands/euca/importkeypair.py
+++ b/euca2ools/commands/euca/importkeypair.py
@@ -28,23 +28,19 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-import base64
+from euca2ools.commands.argtypes import b64encoded_file_contents
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
-from ..argtypes import file_contents
+
 
 class ImportKeyPair(EucalyptusRequest):
-    API_VERSION = '2010-08-31'
-    DESCRIPTION = 'Import a public RSA key'
+    DESCRIPTION = 'Import a public RSA key as a new key pair'
     ARGS = [Arg('KeyName', metavar='KEYPAIR',
-                help='name for the new key pair'),
-            Arg('-f', '--public-key-file', dest='pubkey', metavar='PUBKEY',
-                type=file_contents, required=True, route_to=None,
-                help='file name of the public key to import')]
-
-    def preprocess(self):
-        self.params = {'PublicKeyMaterial':
-                        base64.b64encode(self.args['pubkey'])}
+                help='name for the new key pair (required)'),
+            Arg('-f', '--public-key-file', dest='PublicKeyMaterial',
+                metavar='FILE', type=b64encoded_file_contents, required=True,
+                help='''name of a file containing the public key to import
+                (required)''')]
 
     def print_result(self, result):
         print self.tabify(['KEYPAIR', result.get('keyName'),
diff --git a/euca2ools/commands/euca/modgroup.py b/euca2ools/commands/euca/modgroup.py
index 438f187..4d25d88 100644
--- a/euca2ools/commands/euca/modgroup.py
+++ b/euca2ools/commands/euca/modgroup.py
@@ -1,6 +1,6 @@
 # Software License Agreement (BSD License)
 #
-# Copyright (c) 2012, Eucalyptus Systems, Inc.
+# Copyright (c) 2012-2013, Eucalyptus Systems, Inc.
 # All rights reserved.
 #
 # Redistribution and use of this software in source and binary forms, with or
@@ -28,76 +28,108 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg, MutuallyExclusiveArgList
+from requestbuilder.exceptions import ArgumentError
 import sys
-from . import EucalyptusRequest
+
 
 class ModifySecurityGroupRequest(EucalyptusRequest):
     '''
     The basis for security group-editing commands
     '''
 
-    ARGS = [Arg('GroupName', metavar='GROUP',
-                help='name of the security group to modify'),
+    ARGS = [Arg('group', metavar='GROUP', route_to=None,
+                help='name or ID of the security group to modify (required)'),
+            Arg('--egress', action='store_true', route_to=None,
+                help='''[VPC only] manage an egress rule, which controls
+                traffic leaving the group'''),
             Arg('-P', '--protocol', dest='IpPermissions.1.IpProtocol',
                 choices=['tcp', 'udp', 'icmp', '6', '17', '1'], default='tcp',
                 help='protocol to affect (default: tcp)'),
-            Arg('-p', '--port-range', dest='port_range', route_to=None,
-                help='''range of ports (specified as "from-to") or a single
-                        port'''),
+            Arg('-p', '--port-range', dest='port_range', metavar='RANGE',
+                route_to=None, help='''range of ports (specified as "from-to")
+                or a single port number (required for tcp and udp)'''),
                 # ^ required for tcp and udp
             Arg('-t', '--icmp-type-code', dest='icmp_type_code',
                 metavar='TYPE:CODE', route_to=None,
-                help='ICMP type and code (specified as "type:code")'),
+                help='''ICMP type and code (specified as "type:code") (required
+                for icmp)'''),
                 # ^ required for icmp
             MutuallyExclusiveArgList(
                 Arg('-s', '--cidr', metavar='CIDR',
                     dest='IpPermissions.1.IpRanges.1.CidrIp',
                     help='''IP range (default: 0.0.0.0/0)'''),
                     # ^ default is added by main()
-                Arg('-o', metavar='GROUP',
-                    dest='IpPermissions.1.Groups.1.GroupName',
-                    help='''name of a security group with which to authorize
-                            network communication''')),
-            Arg('-u', metavar='GROUP_USER',
+                Arg('-o', dest='target_group', metavar='GROUP', route_to=None,
+                    help='''[Non-VPC only] name of a security group with which
+                    to affect network communication''')),
+            Arg('-u', metavar='ACCOUNT',
                 dest='IpPermissions.1.Groups.1.UserId',
                 help='''ID of the account that owns the security group
-                        specified with -o''')]
-                # ^ required if -o is used
+                specified with -o''')]
 
     def configure(self):
         EucalyptusRequest.configure(self)
 
+        if (self.args['group'].startswith('sg-') and
+            len(self.args['group']) == 11):
+            # The check could probably be a little better, but meh.  Fix if
+            # needed.
+            self.params['GroupId'] = self.args['group']
+        else:
+            if self.args['egress']:
+                raise ArgumentError('egress rules must use group IDs, not '
+                                    'names')
+            self.params['GroupName'] = self.args['group']
+
+        target_group = self.args.get('target_group')
+        if (target_group is not None and target_group.startswith('sg-') and
+            len(target_group) == 11):
+            # Same note as above
+            self.params['IpPermissions.1.Groups.1.GroupId'] = target_group
+        else:
+            if self.args['egress']:
+                raise ArgumentError('argument -o: egress rules must use group '
+                                    'IDs, not names')
+            self.params['IpPermissions.1.Groups.1.GroupName'] = target_group
+
         from_port = None
         to_port   = None
         protocol = self.args.get('IpPermissions.1.IpProtocol')
         if protocol in ['icmp', '1']:
+            if self.args.get('port_range'):
+                raise ArgumentError('argument -p/--port-range: not compatible '
+                                    'with protocol ' + protocol)
             if not self.args.get('icmp_type_code'):
-                self._cli_parser.error('argument -t/--icmp-type-code is '
-                                       'required for ICMP')
+                raise ArgumentError('argument -t/--icmp-type-code is required '
+                                    'for protocol ' + protocol)
             types = self.args['icmp_type_code'].split(':')
             if len(types) == 2:
                 try:
                     from_port = int(types[0])
                     to_port   = int(types[1])
                 except ValueError:
-                    self._cli_parser.error('argument -t/--icmp-type-code: '
-                                           'value must have format "1:2"')
+                    raise ArgumentError('argument -t/--icmp-type-code: value '
+                                        'must have format "1:2"')
             else:
-                self._cli_parser.error('argument -t/--icmp-type-code: value '
-                                       'must have format "1:2"')
+                raise ArgumentError('argument -t/--icmp-type-code: value must '
+                                    'have format "1:2"')
             if from_port < -1 or to_port < -1:
-                self._cli_parser.error('argument -t/--icmp-type-code: type, '
-                                       'code must be at least -1')
+                raise ArgumentError('argument -t/--icmp-type-code: type, code '
+                                    'must be at least -1')
 
         elif protocol in ['tcp', 'udp', '6', '17']:
+            if self.args.get('icmp_type_code'):
+                raise ArgumentError('argument -t/--icmp-type-code: not '
+                                    'compatible with protocol ' + protocol)
             if not self.args.get('port_range'):
-                self._cli_parser.error('argument -p/--port-range is required '
-                                       'for protocol ' + protocol)
+                raise ArgumentError('argument -p/--port-range is required for '
+                                    'protocol ' + protocol)
             if ':' in self.args['port_range']:
                 # Be extra helpful in the event of this common typo
-                self._cli_parser.error('argument -p/--port-range: multi-port '
-                        'range must be separated by "-", not ":"')
+                raise ArgumentError('argument -p/--port-range: multi-port '
+                                    'range must be separated by "-", not ":"')
             if self.args['port_range'].startswith('-'):
                 ports = self.args['port_range'][1:].split('-')
                 ports[0] = '-' + ports[0]
@@ -108,20 +140,20 @@ class ModifySecurityGroupRequest(EucalyptusRequest):
                     from_port = int(ports[0])
                     to_port   = int(ports[1])
                 except ValueError:
-                    self._cli_parser.error('argument -p/--port-range: '
-                            'multi-port value must be comprised of integers')
+                    raise ArgumentError('argument -p/--port-range: multi-port '
+                                        'value must be comprised of integers')
             elif len(ports) == 1:
                 try:
                     from_port = to_port = int(ports[0])
                 except ValueError:
-                    self._cli_parser.error('argument -p/--port-range: single '
-                                           'port value must be an integer')
+                    raise ArgumentError('argument -p/--port-range: single '
+                                        'port value must be an integer')
             else:
-                self._cli_parser.error('argument -p/--port-range: value must '
-                                       'have format "1" or "1-2"')
+                raise ArgumentError('argument -p/--port-range: value must '
+                                    'have format "1" or "1-2"')
             if from_port < -1 or to_port < -1:
-                self._cli_parser.error('argument -p/--port-range: port '
-                                       'number(s) must be at least -1')
+                raise ArgumentError('argument -p/--port-range: port number(s) '
+                                    'must be at least -1')
         else:
             # Shouldn't get here since argparse should only allow the values we
             # handle
@@ -133,27 +165,32 @@ class ModifySecurityGroupRequest(EucalyptusRequest):
         if not self.args.get('IpPermissions.1.IpRanges.1.GroupName'):
             self.args.setdefault('IpPermissions.1.IpRanges.1.CidrIp',
                                  '0.0.0.0/0')
-        if (self.args.get('IpPermissions.1.Groups.1.GroupName') and
+        if (self.params.get('IpPermissions.1.Groups.1.GroupName') and
             not self.args.get('IpPermissions.1.Groups.1.UserId')):
-            self._cli_parser.error('argument -u is required when -o is '
-                                   'specified')
+            raise ArgumentError('argument -u is required when -o names a '
+                                'security group by name')
 
     def print_result(self, result):
-        print self.tabify(['GROUP', self.args.get('GroupName')])
-        perm_str = ['PERMISSION', self.args.get('GroupName'), 'ALLOWS',
-                    self.args.get('IpPermissions.1.IpProtocol'),
-                    self.args.get('IpPermissions.1.FromPort'),
-                    self.args.get('IpPermissions.1.ToPort')]
-        if self.args.get('IpPermissions.1.Groups.1.UserId'):
+        print self.tabify(['GROUP', self.args.get('group')])
+        perm_str = ['PERMISSION', self.args.get('group'), 'ALLOWS',
+                    self.params.get('IpPermissions.1.IpProtocol'),
+                    self.params.get('IpPermissions.1.FromPort'),
+                    self.params.get('IpPermissions.1.ToPort')]
+        if self.params.get('IpPermissions.1.Groups.1.UserId'):
             perm_str.append('USER')
-            perm_str.append(self.args.get('IpPermissions.1.Groups.1.UserId'))
-        if self.args.get('IpPermissions.1.Groups.1.GroupName'):
+            perm_str.append(self.params.get('IpPermissions.1.Groups.1.UserId'))
+        if self.params.get('IpPermissions.1.Groups.1.GroupId'):
+            perm_str.append('GRPID')
+            perm_str.append(self.params.get(
+                'IpPermissions.1.Groups.1.GroupId'))
+        elif self.params.get('IpPermissions.1.Groups.1.GroupName'):
             perm_str.append('GRPNAME')
-            perm_str.append(self.args.get(
-                    'IpPermissions.1.Groups.1.GroupName'))
-        if self.args.get('IpPermissions.1.IpRanges.1.CidrIp'):
+            perm_str.append(self.params.get(
+                'IpPermissions.1.Groups.1.GroupName'))
+        if self.params.get('IpPermissions.1.IpRanges.1.CidrIp'):
             perm_str.extend(['FROM', 'CIDR'])
-            perm_str.append(self.args.get('IpPermissions.1.IpRanges.1.CidrIp'))
+            perm_str.append(self.params.get(
+                'IpPermissions.1.IpRanges.1.CidrIp'))
         print self.tabify(perm_str)
 
     def process_cli_args(self):
diff --git a/euca2ools/commands/euca/modifyimageattribute.py b/euca2ools/commands/euca/modifyimageattribute.py
index a6265ab..6dcb8ce 100644
--- a/euca2ools/commands/euca/modifyimageattribute.py
+++ b/euca2ools/commands/euca/modifyimageattribute.py
@@ -28,8 +28,10 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg, MutuallyExclusiveArgList
-from . import EucalyptusRequest
+from requestbuilder.exceptions import ArgumentError
+
 
 class ModifyImageAttribute(EucalyptusRequest):
     DESCRIPTION = 'Modify an attribute of an image'
@@ -48,7 +50,7 @@ class ModifyImageAttribute(EucalyptusRequest):
                 "all" for all accounts'''),
             Arg('-r', '--remove', metavar='ENTITY', action='append',
                 default=[], route_to=None, help='''account to remove launch
-                permission from , or "all" for all accounts''')]
+                permission from, or "all" for all accounts''')]
 
     def preprocess(self):
         if self.args.get('launch_permission'):
@@ -66,16 +68,16 @@ class ModifyImageAttribute(EucalyptusRequest):
                 else:
                     lp['Remove'].append({'UserId': entity})
             if not lp:
-                self._cli_parser.error('at least one entity must be specified '
-                                       'with -a/--add or -r/--remove')
+                raise ArgumentError('at least one entity must be specified '
+                                    'with -a/--add or -r/--remove')
             self.params['LaunchPermission'] = lp
         else:
             if self.args.get('add'):
-                self._cli_parser.error('argument -a/--add may only be used '
-                                       'with -l/--launch-permission')
+                raise ArgumentError('argument -a/--add may only be used '
+                                    'with -l/--launch-permission')
             if self.args.get('remove'):
-                self._cli_parser.error('argument -r/--remove may only be used '
-                                       'with -l/--launch-permission')
+                raise ArgumentError('argument -r/--remove may only be used '
+                                    'with -l/--launch-permission')
 
     def print_result(self, result):
         if self.args.get('Description.Value'):
diff --git a/euca2ools/commands/euca/monitorinstances.py b/euca2ools/commands/euca/monitorinstances.py
index d459548..e3b741c 100644
--- a/euca2ools/commands/euca/monitorinstances.py
+++ b/euca2ools/commands/euca/monitorinstances.py
@@ -28,16 +28,17 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class MonitorInstances(EucalyptusRequest):
     DESCRIPTION = 'Enable monitoring for one or more instances'
-    ARGS = [Arg('InstanceId', metavar='INSTANCE', nargs='+',
-                help='instance(s) to monitor')]
+    ARGS = [Arg('InstanceId', metavar='INSTANCE', nargs='+', help='''ID(s) of
+                the instance(s) to begin monitoring (at least 1 required)''')]
     LIST_TAGS = ['instancesSet']
 
     def print_result(self, result):
         for instance in result.get('instancesSet', []):
             print self.tabify((instance.get('instanceId'), 'monitoring-' +
-                    instance.get('monitoring', {}).get('state')))
+                instance.get('monitoring', {}).get('state')))
diff --git a/euca2ools/commands/euca/rebootinstances.py b/euca2ools/commands/euca/rebootinstances.py
index 56e1b46..5e3f8e2 100644
--- a/euca2ools/commands/euca/rebootinstances.py
+++ b/euca2ools/commands/euca/rebootinstances.py
@@ -28,10 +28,11 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class RebootInstances(EucalyptusRequest):
     DESCRIPTION = 'Reboot one or more instances'
-    ARGS = [Arg('InstanceId', metavar='INSTANCE', nargs='+',
-                help='instance(s) to reboot')]
+    ARGS = [Arg('InstanceId', metavar='INSTANCE', nargs='+', help='''ID(s) of
+                the instance(s) to reboot (at least 1 required)''')]
diff --git a/euca2ools/commands/euca/registerimage.py b/euca2ools/commands/euca/registerimage.py
index 8f4fa50..f785a98 100644
--- a/euca2ools/commands/euca/registerimage.py
+++ b/euca2ools/commands/euca/registerimage.py
@@ -28,9 +28,11 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.argtypes import ec2_block_device_mapping
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
-from ..argtypes import ec2_block_device_mapping
+from requestbuilder.exceptions import ArgumentError
+
 
 class RegisterImage(EucalyptusRequest):
     DESCRIPTION = 'Register a new image'
@@ -45,30 +47,31 @@ class RegisterImage(EucalyptusRequest):
                 choices=('i386', 'x86_64', 'armhf'),
                 help='CPU architecture of the new image'),
             Arg('--kernel', dest='KernelId', metavar='KERNEL',
-                help='kernel to associate with the new image'),
+                help='ID of the kernel to associate with the new image'),
             Arg('--ramdisk', dest='RamdiskId', metavar='RAMDISK',
-                help='ramdisk to associate with the new image'),
+                help='ID of the ramdisk to associate with the new image'),
             Arg('--root-device-name', dest='RootDeviceName', metavar='DEVICE',
                 help='root device name (default: /dev/sda1)'),
                 # ^ default is added by main()
-            Arg('--snapshot', route_to=None,
+            Arg('-s', '--snapshot', route_to=None,
                 help='snapshot to use for the root device'),
             Arg('-b', '--block-device-mapping', metavar='DEVICE=MAPPED',
                 dest='BlockDeviceMapping', action='append',
-                type=block_device_mapping, default=[],
+                type=ec2_block_device_mapping, default=[],
                 help='''define a block device mapping for the image, in the
                 form DEVICE=MAPPED, where "MAPPED" is "none", "ephemeral(0-3)",
-                or "[SNAP-ID]:[SIZE]:[true|false]"''')]
+                or
+                "[SNAP-ID]:[SIZE]:[true|false]:[standard|VOLTYPE[:IOPS]]"''')]
 
     def preprocess(self):
         if self.args.get('ImageLocation'):
             # instance-store image
             if self.args.get('RootDeviceName'):
-                self._cli_parser.error('argument --root-device-name: not '
-                        'allowed with argument MANIFEST')
+                raise ArgumentError('argument --root-device-name: not allowed '
+                    'with argument MANIFEST')
             if self.args.get('snapshot'):
-                self._cli_parser.error('argument --snapshot: not allowed '
-                        'with argument MANIFEST')
+                raise ArgumentError('argument --snapshot: not allowed with '
+                    'argument MANIFEST')
         else:
             # Try for an EBS image
             if not self.args.get('RootDeviceName'):
@@ -80,10 +83,9 @@ class RegisterImage(EucalyptusRequest):
                     if (snapshot and
                         snapshot != mapping.get('Ebs', {}).get('SnapshotId')):
                         # The mapping's snapshot differs or doesn't exist
-                        self._cli_parser.error('snapshot ID supplied with '
-                                '--snapshot conflicts with block device '
-                                'mapping for root device ' +
-                                mapping['DeviceName'])
+                        raise ArgumentError('snapshot ID supplied with '
+                            '--snapshot conflicts with block device mapping '
+                            'for root device ' + mapping['DeviceName'])
                     else:
                         # No need to apply --snapshot since the mapping is
                         # already there
@@ -92,10 +94,10 @@ class RegisterImage(EucalyptusRequest):
                 if snapshot:
                     self.args['BlockDeviceMapping'].append(
                             {'DeviceName': self.args['RootDeviceName'],
-                             'Ebs':        {'SnapshotId': snapshot}})
+                             'Ebs': {'SnapshotId': snapshot}})
                 else:
-                    self._cli_parser.error('either a manifest location or a '
-                            'root device snapshot mapping must be specified')
+                    raise ArgumentError('either a manifest location or a root '
+                        'device snapshot mapping must be specified')
 
     def print_result(self, result):
         print self.tabify(('IMAGE', result.get('imageId')))
diff --git a/euca2ools/commands/euca/releaseaddress.py b/euca2ools/commands/euca/releaseaddress.py
index 646f099..a9a8c45 100644
--- a/euca2ools/commands/euca/releaseaddress.py
+++ b/euca2ools/commands/euca/releaseaddress.py
@@ -28,12 +28,30 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+from requestbuilder.exceptions import ArgumentError
+
 
 class ReleaseAddress(EucalyptusRequest):
     DESCRIPTION = 'Release an elastic IP address'
-    ARGS = [Arg('PublicIp', metavar='IP', help='elastic IP to release')]
+    ARGS = [Arg('PublicIp', metavar='ADDRESS', nargs='?',
+                help='[Non-VPC only] address to release (required)'),
+            Arg('-a', '--allocation-id', dest='AllocationId', metavar='ALLOC',
+                help='''[VPC only] allocation ID for the address to release
+                (required)''')]
+
+    def configure(self):
+        if (self.args.get('PublicIp') is not None and
+            self.args.get('AllocationId') is not None):
+            # Can't be both EC2 and VPC
+            raise ArgumentError(
+                'argument -a/--allocation-id: not allowed with an IP address')
+        if (self.args.get('PublicIp') is None and
+            self.args.get('AllocationId') is None):
+            # ...but we still have to be one of them
+            raise ArgumentError(
+                'argument -a/--allocation-id or an IP address is required')
 
     def print_result(self, result):
         print self.tabify(('ADDRESS', self.args.get('PublicIp'),
diff --git a/euca2ools/commands/euca/resetimageattribute.py b/euca2ools/commands/euca/resetimageattribute.py
index 474b300..0abd935 100644
--- a/euca2ools/commands/euca/resetimageattribute.py
+++ b/euca2ools/commands/euca/resetimageattribute.py
@@ -29,12 +29,13 @@
 # POSSIBILITY OF SUCH DAMAGE.
 
 from requestbuilder import Arg
-from . import EucalyptusRequest
+from euca2ools.commands.euca import EucalyptusRequest
+
 
 class ResetImageAttribute(EucalyptusRequest):
     DESCRIPTION = 'Reset an attribute of an image to its default value'
     ARGS = [Arg('ImageId', metavar='IMAGE',
-            help='image whose attribute should be reset'),
+            help='ID of the image whose attribute should be reset (required)'),
             Arg('-l', '--launch-permission', dest='Attribute',
                 action='store_const', const='launchPermission', required=True,
                 help='reset launch permissions')]
diff --git a/euca2ools/commands/euca/revoke.py b/euca2ools/commands/euca/revoke.py
index c1630c6..0841ad6 100644
--- a/euca2ools/commands/euca/revoke.py
+++ b/euca2ools/commands/euca/revoke.py
@@ -28,8 +28,15 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-from .modgroup import ModifySecurityGroupRequest
+from euca2ools.commands.euca.modgroup import ModifySecurityGroupRequest
+
 
 class Revoke(ModifySecurityGroupRequest):
-    NAME = 'RevokeSecurityGroupIngress'
-    DESCRIPTION = 'Revoke an existing rule from a security group'
+    DESCRIPTION = 'Remove a rule from a security group'
+
+    @property
+    def action(self):
+        if self.args['egress']:
+            return 'RevokeSecurityGroupEgress'
+        else:
+            return 'RevokeSecurityGroupIngress'
diff --git a/euca2ools/commands/euca/runinstances.py b/euca2ools/commands/euca/runinstances.py
index b74fdf9..5c1a066 100644
--- a/euca2ools/commands/euca/runinstances.py
+++ b/euca2ools/commands/euca/runinstances.py
@@ -30,15 +30,19 @@
 
 import argparse
 import base64
+from euca2ools.commands.argtypes import (b64encoded_file_contents,
+    ec2_block_device_mapping, vpc_interface)
+from euca2ools.commands.euca import EucalyptusRequest
 import os.path
 from requestbuilder import Arg, MutuallyExclusiveArgList
+from requestbuilder.exceptions import ArgumentError
 import sys
-from . import EucalyptusRequest
-from ..argtypes import b64encoded_file_contents, ec2_block_device_mapping
+
 
 class RunInstances(EucalyptusRequest):
     DESCRIPTION = 'Launch instances of a machine image'
-    ARGS = [Arg('ImageId', metavar='IMAGE', help='image to instantiate'),
+    ARGS = [Arg('ImageId', metavar='IMAGE',
+                help='ID of the image to instantiate (required)'),
             Arg('-n', '--instance-count', dest='count', metavar='MIN[-MAX]',
                 default='1', route_to=None,
                 help='''number of instances to launch. If this number of
@@ -63,36 +67,84 @@ class RunInstances(EucalyptusRequest):
                     help='''file containing user data to make available to the
                             instances in this reservation''')),
             Arg('--addressing', dest='AddressingType',
-                choices=('public', 'private'),
-                help=('addressing scheme to launch the instance with.  Use '
-                      '"private" to run an instance with no public address.')),
+                choices=('public', 'private'), help='''[Eucalyptus only]
+                addressing scheme to launch the instance with.  Use "private"
+                to run an instance with no public address.'''),
             Arg('-t', '--instance-type', dest='InstanceType',
                 help='type of instance to launch'),
+            Arg('-z', '--availability-zone', metavar='ZONE',
+                dest='Placement.AvailabilityZone'),
             Arg('--kernel', dest='KernelId', metavar='KERNEL',
-                help='kernel to launch the instance(s) with'),
+                help='ID of the kernel to launch the instance(s) with'),
             Arg('--ramdisk', dest='RamdiskId', metavar='RAMDISK',
-                help='ramdisk to launch the instance(s) with'),
+                help='ID of the ramdisk to launch the instance(s) with'),
             Arg('-b', '--block-device-mapping', metavar='DEVICE=MAPPED',
                 dest='BlockDeviceMapping', action='append',
-                type=block_device_mapping, default=[],
+                type=ec2_block_device_mapping, default=[],
                 help='''define a block device mapping for the instances, in the
-                        form DEVICE=MAPPED, where "MAPPED" is "none",
-                        "ephemeral(0-3)", or
-                        "[SNAP-ID]:[SIZE]:[true|false]"'''),
+                form DEVICE=MAPPED, where "MAPPED" is "none", "ephemeral(0-3)",
+                or
+                "[SNAP-ID]:[SIZE]:[true|false]:[standard|VOLTYPE[:IOPS]]"'''),
             Arg('-m', '--monitor', dest='Monitoring.Enabled',
                 action='store_const', const='true',
                 help='enable detailed monitoring for the instance(s)'),
-            Arg('--subnet', dest='SubnetId', metavar='SUBNET',
-                help='VPC subnet in which to launch the instance(s)'),
-            Arg('-z', '--availability-zone', metavar='ZONE',
-                dest='Placement.AvailabilityZone'),
+            Arg('--disable-api-termination', dest='DisableApiTermination',
+                action='store_const', const='true',
+                help='prevent API users from terminating the instance(s)'),
             Arg('--instance-initiated-shutdown-behavior',
                 dest='InstanceInitiatedShutdownBehavior',
                 choices=('stop', 'terminate'),
                 help=('whether to "stop" (default) or terminate EBS instances '
-                      'when they shut down'))]
+                      'when they shut down')),
+            Arg('--placement-group', dest='Placement.GroupName',
+                metavar='PLGROUP', help='''name of a placement group to launch
+                into'''),
+            Arg('--tenancy', dest='Placement.Tenancy',
+                choices=('default', 'dedicated'), help='''[VPC only]
+                "dedicated" to run on single-tenant hardware'''),
+            Arg('--client-token', dest='ClientToken', metavar='TOKEN',
+                help='unique identifier to ensure request idempotency'),
+            Arg('-s', '--subnet', metavar='SUBNET', route_to=None,
+                help='''[VPC only] subnet to create the instance's network
+                interface in'''),
+            Arg('--private-ip-address', metavar='ADDRESS', route_to=None,
+                help='''[VPC only] assign a specific primary private IP address
+                to an instance's interface'''),
+            MutuallyExclusiveArgList(
+                Arg('--secondary-private-ip-address', metavar='ADDRESS',
+                    action='append', route_to=None, help='''[VPC only]
+                    assign a specific secondary private IP address to an
+                    instance's network interface.  Use this option multiple
+                    times to add additional addresses.'''),
+                Arg('--secondary-private-ip-address-count', metavar='COUNT',
+                    type=int, route_to=None, help='''[VPC only]
+                    automatically assign a specific number of secondary private
+                    IP addresses to an instance's network interface''')),
+            Arg('-a', '--network-interface', dest='NetworkInterface',
+                metavar='INTERFACE', action='append', type=vpc_interface,
+                help='''[VPC only] add a network interface to the new
+                instance.  If the interface already exists, supply its ID and
+                a numeric index for it, separated by ":", in the form
+                "eni-XXXXXXXX:INDEX".  To create a new interface, supply a
+                numeric index and subnet ID for it, along with (in order) an
+                optional description, a primary private IP address, a list of
+                security group IDs to associate with the interface, whether to
+                delete the interface upon instance termination ("true" or
+                "false"), a number of secondary private IP addresses to create
+                automatically, and a list of secondary private IP addresses to
+                assign to the interface, separated by ":", in the form
+                ":INDEX:SUBNET:[DESCRIPTION]:[PRIV_IP]:[GROUP1,GROUP2,...]:[true|false]:[SEC_IP_COUNT|:SEC_IP1,SEC_IP2,...]".  You cannot specify both of the
+                latter two.  This option may be used multiple times.  Each adds
+                another network interface.'''),
+            Arg('-p', '--iam-profile', metavar='IPROFILE', route_to=None,
+                help='''name or ARN of the IAM instance profile to associate
+                with the new instance(s)'''),
+            Arg('--ebs-optimized', dest='EbsOptimized', action='store_const',
+                const='true', help='optimize the new instance(s) for EBS I/O')]
+
     LIST_TAGS = ['reservationSet', 'instancesSet', 'groupSet', 'tagSet',
-                 'blockDeviceMapping', 'productCodes']
+                 'blockDeviceMapping', 'productCodes', 'networkInterfaceSet',
+                 'attachment', 'association', 'privateIpAddressesSet']
 
     def preprocess(self):
         counts = self.args['count'].split('-')
@@ -101,22 +153,22 @@ class RunInstances(EucalyptusRequest):
                 self.params['MinCount'] = int(counts[0])
                 self.params['MaxCount'] = int(counts[0])
             except ValueError:
-                self._cli_parser.error('argument -n/--instance-count: '
-                                       'instance count must be an integer')
+                raise ArgumentError('argument -n/--instance-count: instance '
+                                    'count must be an integer')
         elif len(counts) == 2:
             try:
                 self.params['MinCount'] = int(counts[0])
                 self.params['MaxCount'] = int(counts[1])
             except ValueError:
-                self._cli_parser.error('argument -n/--instance-count: '
-                        'instance count range must be must be comprised of '
-                        'integers')
+                raise ArgumentError('argument -n/--instance-count: instance '
+                                    'count range must be must be comprised of '
+                                    'integers')
         else:
-            self._cli_parser.error('argument -n/--instance-count: value must '
-                                   'have format "1" or "1-2"')
+            raise ArgumentError('argument -n/--instance-count: value must '
+                                'have format "1" or "1-2"')
         if self.params['MinCount'] < 1 or self.params['MaxCount'] < 1:
-            self._cli_parser.error('argument -n/--instance-count: instance '
-                                   'count must be positive')
+            raise ArgumentError('argument -n/--instance-count: instance count '
+                                'must be positive')
         if self.params['MinCount'] > self.params['MaxCount']:
             self.log.debug('MinCount > MaxCount; swapping')
             self.params.update({'MinCount': self.params['MaxCount'],
@@ -130,5 +182,33 @@ class RunInstances(EucalyptusRequest):
                 self.params.setdefault('SecurityGroup', [])
                 self.params['SecurityGroup'].append(group)
 
+        iprofile = self.args.get('iam_profile')
+        if iprofile:
+            if iprofile.startswith('arn:'):
+                self.params['IamInstanceProfile.Arn'] = iprofile
+            else:
+                self.params['IamInstanceProfile.Name'] = iprofile
+
+        # Assemble an interface out of the "friendly" split interface options
+        cli_iface = {}
+        if self.args.get('private_ip_address'):
+            cli_iface['PrivateIpAddresses'] = [
+                {'PrivateIpAddress': self.args['private_ip_address'],
+                 'Primary': 'true'}]
+        if self.args.get('secondary_private_ip_address'):
+            sec_ips = [{'PrivateIpAddress': addr} for addr in
+                       self.args['secondary_private_ip_address']]
+            cli_iface.setdefault('PrivateIpAddresses', [])
+            cli_iface['PrivateIpAddresses'].extend(sec_ips)
+        if self.args.get('secondary_private_ip_address_count'):
+            sec_ip_count = self.args['secondary_private_ip_address_count']
+            cli_iface['SecondaryPrivateIpAddressCount'] = sec_ip_count
+        if self.args.get('subnet'):
+            cli_iface['SubnetId'] = self.args['subnet']
+        if cli_iface:
+            cli_iface['DeviceIndex'] = 0
+            self.params.setdefault('NetworkInterface', [])
+            self.params['NetworkInterface'].append(cli_iface)
+
     def print_result(self, result):
         self.print_reservation(result)
diff --git a/euca2ools/commands/euca/startinstances.py b/euca2ools/commands/euca/startinstances.py
index f4d0f1d..e14168d 100644
--- a/euca2ools/commands/euca/startinstances.py
+++ b/euca2ools/commands/euca/startinstances.py
@@ -28,13 +28,14 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class StartInstances(EucalyptusRequest):
     DESCRIPTION = 'Start one or more stopped instances'
     ARGS = [Arg('InstanceId', metavar='INSTANCE', nargs='+',
-                help='instance(s) to start')]
+                help='ID(s) of the instance(s) to start')]
     LIST_TAGS = ['instancesSet']
 
     def print_result(self, result):
diff --git a/euca2ools/commands/euca/stopinstances.py b/euca2ools/commands/euca/stopinstances.py
index 32426d9..c677a20 100644
--- a/euca2ools/commands/euca/stopinstances.py
+++ b/euca2ools/commands/euca/stopinstances.py
@@ -28,16 +28,17 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class StopInstances(EucalyptusRequest):
     DESCRIPTION = 'Stop one or more running instances'
     ARGS = [Arg('InstanceId', metavar='INSTANCE', nargs='+',
-                help='instance(s) to stop'),
+                help='ID(s) of the instance(s) to stop'),
             Arg('-f', '--force', dest='Force', action='store_const',
                 const='true',
-                help='immediately stop the instance. Data may be lost')]
+                help='immediately stop the instance(s). Data may be lost')]
     LIST_TAGS = ['instancesSet']
 
     def print_result(self, result):
diff --git a/euca2ools/commands/euca/terminateinstances.py b/euca2ools/commands/euca/terminateinstances.py
index 9021aa1..97fd42b 100644
--- a/euca2ools/commands/euca/terminateinstances.py
+++ b/euca2ools/commands/euca/terminateinstances.py
@@ -28,13 +28,14 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class TerminateInstances(EucalyptusRequest):
     DESCRIPTION = 'Terminate one or more instances'
     ARGS = [Arg('InstanceId', metavar='INSTANCE', nargs='+',
-                help='instance(s) to terminate')]
+                help='ID(s) of the instance(s) to terminate')]
     LIST_TAGS = ['instancesSet']
 
     def print_result(self, result):
diff --git a/euca2ools/commands/euca/unmonitorinstances.py b/euca2ools/commands/euca/unmonitorinstances.py
index bcb3ce0..a3a458e 100644
--- a/euca2ools/commands/euca/unmonitorinstances.py
+++ b/euca2ools/commands/euca/unmonitorinstances.py
@@ -28,13 +28,14 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from euca2ools.commands.euca import EucalyptusRequest
 from requestbuilder import Arg
-from . import EucalyptusRequest
+
 
 class UnmonitorInstances(EucalyptusRequest):
     DESCRIPTION = 'Disable monitoring for one or more instances'
-    ARGS = [Arg('InstanceId', metavar='INSTANCE', nargs='+',
-                help='instance(s) to un-monitor')]
+    ARGS = [Arg('InstanceId', metavar='INSTANCE', nargs='+', help='''ID(s) ofthe
+                the instance(s) to stop monitoring (at least 1 required)''')]
     LIST_TAGS = ['instancesSet']
 
     def print_result(self, result):

-- 
managing cloud instances for Eucalyptus



More information about the pkg-eucalyptus-commits mailing list