Add full support for list and register in glance
authorGiorgos Verigakis <verigak@gmail.com>
Thu, 12 Jan 2012 15:51:09 +0000 (17:51 +0200)
committerGiorgos Verigakis <verigak@gmail.com>
Thu, 12 Jan 2012 15:51:09 +0000 (17:51 +0200)
kamaki/cli.py
kamaki/client.py
kamaki/utils.py

index 1766745..eac55bb 100755 (executable)
@@ -76,7 +76,7 @@ from grp import getgrgid
 from optparse import OptionParser
 from pwd import getpwuid
 
-from kamaki.client import ComputeClient, ImagesClient, ClientError
+from kamaki.client import ComputeClient, GlanceClient, ClientError
 from kamaki.config import Config, ConfigError
 from kamaki.utils import OrderedDict, print_addresses, print_dict, print_items
 
@@ -89,7 +89,7 @@ CONFIG_ENV = 'KAMAKI_CONFIG'
 
 # The defaults also determine the allowed keys
 CONFIG_DEFAULTS = {
-    'apis': 'nova synnefo glance plankton',
+    'apis': 'nova synnefo glance',
     'token': '',
     'compute_url': 'https://okeanos.grnet.gr/api/v1',
     'images_url': 'https://okeanos.grnet.gr/plankton',
@@ -176,7 +176,7 @@ class server_list(object):
     
     def main(self):
         servers = self.client.list_servers(self.options.detail)
-        print_items(servers, self.options.detail)
+        print_items(servers)
 
 
 @command(api='nova')
@@ -360,7 +360,7 @@ class flavor_list(object):
     
     def main(self):
         flavors = self.client.list_flavors(self.options.detail)
-        print_items(flavors, self.options.detail)
+        print_items(flavors)
 
 
 @command(api='nova')
@@ -383,7 +383,7 @@ class image_list(object):
     
     def main(self):
         images = self.client.list_images(self.options.detail)
-        print_items(images, self.options.detail)
+        print_items(images)
 
 
 @command(api='nova')
@@ -459,7 +459,7 @@ class network_list(object):
     
     def main(self):
         networks = self.client.list_networks(self.options.detail)
-        print_items(networks, self.options.detail)
+        print_items(networks)
 
 
 @command(api='synnefo')
@@ -516,9 +516,80 @@ class network_disconnect(object):
 class glance_list(object):
     """list images"""
     
+    @classmethod
+    def update_parser(cls, parser):
+        parser.add_option('-l', dest='detail', action='store_true',
+                default=False, help='show detailed output')
+        parser.add_option('--container-format', dest='container_format',
+                metavar='FORMAT', help='filter by container format')
+        parser.add_option('--disk-format', dest='disk_format',
+                metavar='FORMAT', help='filter by disk format')
+        parser.add_option('--name', dest='name', metavar='NAME',
+                help='filter by name')
+        parser.add_option('--size-min', dest='size_min', metavar='BYTES',
+                help='filter by minimum size')
+        parser.add_option('--size-max', dest='size_max', metavar='BYTES',
+                help='filter by maximum size')
+        parser.add_option('--status', dest='status', metavar='STATUS',
+                help='filter by status')
+        parser.add_option('--order', dest='order', metavar='FIELD',
+                help='order by FIELD (use a - prefix to reverse order)')
+    
     def main(self):
-        images = self.client.list_public()
-        print images
+        filters = {}
+        for filter in ('container_format', 'disk_format', 'name', 'size_min',
+                       'size_max', 'status'):
+            val = getattr(self.options, filter, None)
+            if val is not None:
+                filters[filter] = val
+        
+        order = self.options.order or ''
+        images = self.client.list_public(self.options.detail, filters=filters,
+                                         order=order)
+        print_items(images, title=('name',))
+
+
+@command(api='glance')
+class glance_register(object):
+    """register an image"""
+    
+    @classmethod
+    def update_parser(cls, parser):
+        parser.add_option('--checksum', dest='checksum', metavar='CHECKSUM',
+                help='set image checksum')
+        parser.add_option('--container-format', dest='container_format',
+                metavar='FORMAT', help='set container format')
+        parser.add_option('--disk-format', dest='disk_format',
+                metavar='FORMAT', help='set disk format')
+        parser.add_option('--id', dest='id',
+                metavar='ID', help='set image ID')
+        parser.add_option('--owner', dest='owner',
+                metavar='USER', help='set image owner (admin only)')
+        parser.add_option('--property', dest='properties', action='append',
+                metavar='KEY=VAL',
+                help='add a property (can be used multiple times)')
+        parser.add_option('--public', dest='is_public', action='store_true',
+                help='mark image as public')
+        parser.add_option('--size', dest='size', metavar='SIZE',
+                help='set image size')
+    
+    def main(self, name, location):
+        params = {}
+        for key in ('checksum', 'container_format', 'disk_format', 'id',
+                    'owner', 'is_public', 'size'):
+            val = getattr(self.options, key)
+            if val is not None:
+                params[key] = val
+        
+        properties = {}
+        for property in self.options.properties or []:
+            key, sep, val = property.partition('=')
+            if not sep:
+                log.error("Invalid property '%s'", property)
+                return 1
+            properties[key.strip()] = val.strip()
+        
+        self.client.register(name, location, params, properties)
 
 
 def print_groups(groups):
@@ -541,7 +612,7 @@ def main():
     parser.usage = '%prog <group> <command> [options]'
     parser.add_option('--help', dest='help', action='store_true',
             default=False, help='show this help message and exit')
-    parser.add_option('--api', dest='apis', metavar='API', action='append',
+    parser.add_option('--api', dest='apis', action='append', metavar='API',
             help='API to use (can be used multiple times)')
     parser.add_option('--compute-url', dest='compute_url', metavar='URL',
             help='URL for the compute API')
@@ -642,16 +713,19 @@ def main():
         url = config.get('compute_url')
         token = config.get('token')
         cmd.client = ComputeClient(url, token)
-    elif cmd.api in ('glance', 'plankton'):
+    elif cmd.api == 'glance':
         url = config.get('images_url')
         token = config.get('token')
-        cmd.client = ImagesClient(url, token)
+        cmd.client = GlanceClient(url, token)
     
     try:
         return cmd.main(*args[3:])
-    except TypeError:
-        parser.print_help()
-        return 1
+    except TypeError as e:
+        if e.args and e.args[0].startswith('main()'):
+            parser.print_help()
+            return 1
+        else:
+            raise
     except ClientError, err:
         log.error('%s', err.message)
         log.info('%s', err.details)
index 2481fea..ffe0bdd 100644 (file)
@@ -35,6 +35,7 @@ import json
 import logging
 
 from httplib import HTTPConnection, HTTPSConnection
+from urllib import quote
 from urlparse import urlparse
 
 
@@ -64,7 +65,7 @@ class Client(object):
         self.url = url
         self.token = token
     
-    def _cmd(self, method, path, body=None, success=200):
+    def http_cmd(self, method, path, body=None, headers=None, success=200):
         p = urlparse(self.url)
         path = p.path + path
         if p.scheme == 'http':
@@ -74,7 +75,8 @@ class Client(object):
         else:
             raise ClientError('Unknown URL scheme')
         
-        headers = {'X-Auth-Token': self.token}
+        headers = headers or {}
+        headers['X-Auth-Token'] = self.token
         if body:
             headers['Content-Type'] = 'application/json'
             headers['Content-Length'] = len(body)
@@ -118,17 +120,17 @@ class Client(object):
 
         return reply
     
-    def _get(self, path, success=200):
-        return self._cmd('GET', path, None, success)
+    def http_get(self, path, success=200):
+        return self.http_cmd('GET', path, success=success)
     
-    def _post(self, path, body, success=202):
-        return self._cmd('POST', path, body, success)
+    def http_post(self, path, body=None, headers=None, success=202):
+        return self.http_cmd('POST', path, body, headers, success)
     
-    def _put(self, path, body, success=204):
-        return self._cmd('PUT', path, body, success)
+    def http_put(self, path, body, success=204):
+        return self.http_cmd('PUT', path, body, success=success)
     
-    def _delete(self, path, success=204):
-        return self._cmd('DELETE', path, None, success)
+    def http_delete(self, path, success=204):
+        return self.http_cmd('DELETE', path, success=success)
 
 
 class ComputeClient(Client):
@@ -137,13 +139,13 @@ class ComputeClient(Client):
     def list_servers(self, detail=False):
         """List servers, returned detailed output if detailed is True"""
         path = '/servers/detail' if detail else '/servers'
-        reply = self._get(path)
+        reply = self.http_get(path)
         return reply['servers']['values']
     
     def get_server_details(self, server_id):
         """Return detailed output on a server specified by its id"""
         path = '/servers/%d' % server_id
-        reply = self._get(path)
+        reply = self.http_get(path)
         return reply['server']
     
     def create_server(self, name, flavor_id, image_id, personality=None):
@@ -165,7 +167,7 @@ class ComputeClient(Client):
             req['personality'] = personality
         
         body = json.dumps({'server': req})
-        reply = self._post('/servers', body)
+        reply = self.http_post('/servers', body)
         return reply['server']
     
     def update_server_name(self, server_id, new_name):
@@ -177,37 +179,37 @@ class ComputeClient(Client):
         """
         path = '/servers/%d' % server_id
         body = json.dumps({'server': {'name': new_name}})
-        self._put(path, body)
+        self.http_put(path, body)
     
     def delete_server(self, server_id):
         """Submit a deletion request for a server specified by id"""
         path = '/servers/%d' % server_id
-        self._delete(path)
+        self.http_delete(path)
     
     def reboot_server(self, server_id, hard=False):
         """Submit a reboot request for a server specified by id"""
         path = '/servers/%d/action' % server_id
         type = 'HARD' if hard else 'SOFT'
         body = json.dumps({'reboot': {'type': type}})
-        self._post(path, body)
+        self.http_post(path, body)
     
     def start_server(self, server_id):
         """Submit a startup request for a server specified by id"""
         path = '/servers/%d/action' % server_id
         body = json.dumps({'start': {}})
-        self._post(path, body)
+        self.http_post(path, body)
     
     def shutdown_server(self, server_id):
         """Submit a shutdown request for a server specified by id"""
         path = '/servers/%d/action' % server_id
         body = json.dumps({'shutdown': {}})
-        self._post(path, body)
+        self.http_post(path, body)
     
     def get_server_console(self, server_id):
         """Get a VNC connection to the console of a server specified by id"""
         path = '/servers/%d/action' % server_id
         body = json.dumps({'console': {'type': 'vnc'}})
-        reply = self._post(path, body, 200)
+        reply = self.http_post(path, body, success=200)
         return reply['console']
     
     def set_firewall_profile(self, server_id, profile):
@@ -215,45 +217,44 @@ class ComputeClient(Client):
 
         The server is specified by id, the profile argument
         is one of (ENABLED, DISABLED, PROTECTED).
-
         """
         path = '/servers/%d/action' % server_id
         body = json.dumps({'firewallProfile': {'profile': profile}})
-        self._post(path, body, 202)
+        self.http_post(path, body)
     
     def list_server_addresses(self, server_id, network=None):
         path = '/servers/%d/ips' % server_id
         if network:
             path += '/%s' % network
-        reply = self._get(path)
+        reply = self.http_get(path)
         return [reply['network']] if network else reply['addresses']['values']
     
     def get_server_metadata(self, server_id, key=None):
         path = '/servers/%d/meta' % server_id
         if key:
             path += '/%s' % key
-        reply = self._get(path)
+        reply = self.http_get(path)
         return reply['meta'] if key else reply['metadata']['values']
     
     def create_server_metadata(self, server_id, key, val):
         path = '/servers/%d/meta/%s' % (server_id, key)
         body = json.dumps({'meta': {key: val}})
-        reply = self._put(path, body, 201)
+        reply = self.http_put(path, body, 201)
         return reply['meta']
     
     def update_server_metadata(self, server_id, **metadata):
         path = '/servers/%d/meta' % server_id
         body = json.dumps({'metadata': metadata})
-        reply = self._post(path, body, 201)
+        reply = self.http_post(path, body, success=201)
         return reply['metadata']
     
     def delete_server_metadata(self, server_id, key):
         path = '/servers/%d/meta/%s' % (server_id, key)
-        reply = self._delete(path)
+        reply = self.http_delete(path)
     
     def get_server_stats(self, server_id):
         path = '/servers/%d/stats' % server_id
-        reply = self._get(path)
+        reply = self.http_get(path)
         return reply['stats']
     
     
@@ -261,12 +262,12 @@ class ComputeClient(Client):
     
     def list_flavors(self, detail=False):
         path = '/flavors/detail' if detail else '/flavors'
-        reply = self._get(path)
+        reply = self.http_get(path)
         return reply['flavors']['values']
 
     def get_flavor_details(self, flavor_id):
         path = '/flavors/%d' % flavor_id
-        reply = self._get(path)
+        reply = self.http_get(path)
         return reply['flavor']
     
     
@@ -274,86 +275,114 @@ class ComputeClient(Client):
     
     def list_images(self, detail=False):
         path = '/images/detail' if detail else '/images'
-        reply = self._get(path)
+        reply = self.http_get(path)
         return reply['images']['values']
 
     def get_image_details(self, image_id):
         path = '/images/%d' % image_id
-        reply = self._get(path)
+        reply = self.http_get(path)
         return reply['image']
 
     def create_image(self, server_id, name):
         req = {'name': name, 'serverRef': server_id}
         body = json.dumps({'image': req})
-        reply = self._post('/images', body)
+        reply = self.http_post('/images', body)
         return reply['image']
 
     def delete_image(self, image_id):
         path = '/images/%d' % image_id
-        self._delete(path)
+        self.http_delete(path)
 
     def get_image_metadata(self, image_id, key=None):
         path = '/images/%d/meta' % image_id
         if key:
             path += '/%s' % key
-        reply = self._get(path)
+        reply = self.http_get(path)
         return reply['meta'] if key else reply['metadata']['values']
     
     def create_image_metadata(self, image_id, key, val):
         path = '/images/%d/meta/%s' % (image_id, key)
         body = json.dumps({'meta': {key: val}})
-        reply = self._put(path, body, 201)
+        reply = self.http_put(path, body, 201)
         reply['meta']
 
     def update_image_metadata(self, image_id, **metadata):
         path = '/images/%d/meta' % image_id
         body = json.dumps({'metadata': metadata})
-        reply = self._post(path, body, 201)
+        reply = self.http_post(path, body, success=201)
         return reply['metadata']
 
     def delete_image_metadata(self, image_id, key):
         path = '/images/%d/meta/%s' % (image_id, key)
-        reply = self._delete(path)
+        reply = self.http_delete(path)
     
     
     # Networks
     
     def list_networks(self, detail=False):
         path = '/networks/detail' if detail else '/networks'
-        reply = self._get(path)
+        reply = self.http_get(path)
         return reply['networks']['values']
     
     def create_network(self, name):
         body = json.dumps({'network': {'name': name}})
-        reply = self._post('/networks', body)
+        reply = self.http_post('/networks', body)
         return reply['network']
     
     def get_network_details(self, network_id):
         path = '/networks/%s' % network_id
-        reply = self._get(path)
+        reply = self.http_get(path)
         return reply['network']
     
     def update_network_name(self, network_id, new_name):
         path = '/networks/%s' % network_id
         body = json.dumps({'network': {'name': new_name}})
-        self._put(path, body)
+        self.http_put(path, body)
     
     def delete_network(self, network_id):
         path = '/networks/%s' % network_id
-        self._delete(path)
+        self.http_delete(path)
 
     def connect_server(self, server_id, network_id):
         path = '/networks/%s/action' % network_id
         body = json.dumps({'add': {'serverRef': server_id}})
-        self._post(path, body)
+        self.http_post(path, body)
     
     def disconnect_server(self, server_id, network_id):
         path = '/networks/%s/action' % network_id
         body = json.dumps({'remove': {'serverRef': server_id}})
-        self._post(path, body)
+        self.http_post(path, body)
 
 
-class ImagesClient(Client):
-    def list_public(self, detail=False):
+class GlanceClient(Client):
+    def list_public(self, detail=False, filters={}, order=''):
         path = '/images/detail' if detail else '/images/'
-        return self._get(path)
+        params = {}
+        params.update(filters)
+        
+        if order.startswith('-'):
+            params['sort_dir'] = 'desc'
+            order = order[1:]
+        else:
+            params['sort_dir'] = 'asc'
+        
+        if order:
+            params['sort_key'] = order
+        
+        if params:
+            path += '?' + '&'.join('%s=%s' % item for item in params.items())
+        return self.http_get(path)
+
+    def register(self, name, location, params={}, properties={}):
+        path = '/images/'
+        headers = {}
+        headers['x-image-meta-name'] = quote(name)
+        headers['x-image-meta-location'] = location
+        for key, val in params.items():
+            if key in ('id', 'store', 'disk_format', 'container_format',
+                       'size', 'checksum', 'is_public', 'owner'):
+                key = 'x-image-meta-' + key.replace('_', '-')
+                headers[key] = val
+        for key, val in properties.items():
+            headers['x-image-meta-property-' + quote(key)] = quote(val)
+        return self.http_post(path, headers=headers, success=200)
index 6aa7ccf..ffe9647 100644 (file)
@@ -85,37 +85,35 @@ def print_addresses(addresses, margin):
             print '%s: %s' % (key.rjust(margin + 8), ip['addr'])
 
 
-def print_metadata(metadata, margin):
-    print '%s:' % 'metadata'.rjust(margin)
-    for key, val in metadata.get('values', {}).items():
-        print '%s: %s' % (key.rjust(margin + 4), val)
-
-
 def print_dict(d, exclude=()):
     if not d:
         return
     margin = max(len(key) for key in d) + 1
-
+    
     for key, val in sorted(d.items()):
         if key in exclude:
             continue
-
+        
         if key == 'addresses':
             print '%s:' % 'addresses'.rjust(margin)
             print_addresses(val.get('values', []), margin)
             continue
-        elif key == 'metadata':
-            print_metadata(val, margin)
-            continue
         elif key == 'servers':
             val = ', '.join(str(x) for x in val['values'])
-
+        elif isinstance(val, dict):
+            if val.keys() == ['values']:
+                val = val['values']
+            print '%s:' % key.rjust(margin)
+            for key, val in val.items():
+                print '%s: %s' % (key.rjust(margin + 4), val)
+            continue
+        
         print '%s: %s' % (key.rjust(margin), val)
 
 
-def print_items(items, detail=False):
+def print_items(items, title=('id', 'name')):
     for item in items:
-        print '%s %s' % (item['id'], item.get('name', ''))
-        if detail:
-            print_dict(item, exclude=('id', 'name'))
+        print ' '.join(str(item.pop(key)) for key in title if key in item)
+        if item:
+            print_dict(item)
             print