Revision aa82dd5a

b/Changelog
74 74
- Store image properties on remote location after image registration [#3769]
75 75
- Add runtime args to image register for forcing or unsettitng property
76 76
    storage [#3769]
77
- Expand runtime args of image register for managing metadata and metada file
78
    dumps and loads [#3797]
77 79
- Add server-firewall-get command to get a VMs firewall profile
78 80

  
b/kamaki/cli/commands/errors.py
358 358
        '* get a list of image ids: /image list',
359 359
        '* details of image: /flavor info <image id>']
360 360

  
361
    remote_image_file = [
362
        'Suggested usage:',
363
        '  /image register <image container>:<uploaded image file path>',
364
        'To set "image" as image container and "my_dir/img.diskdump" as',
365
        'the remote image file path, try one of the following:',
366
        '- <image container>:<remote path>',
367
        '    e.g. image:/my_dir/img.diskdump',
368
        '- <remote path> -C <image container>',
369
        '    e.g. /my_dir/img.diskdump -C image',
370
        'To check if the image file is accessible to current user:',
371
        '  /file list <image container>',
372
        'If the file is located under a different user id "us3r1d"',
373
        ' use the --fileowner=us3r1d  argument e.g.:',
374
        '  /image register "my" image:my_dir/img.diskdump --fileowner=us3r1d',
375
        'Note: The form pithos://<userid>/<container>/<path> is deprecated']
376

  
377 361
    @classmethod
378 362
    def connection(this, foo):
379 363
        return generic._connection(foo, 'image.url')
......
412 396
                raise
413 397
        return _raise
414 398

  
415
    @classmethod
416
    def image_file(this, foo):
417
        def _raise(self, name, container_path):
418
            try:
419
                return foo(self, name, container_path)
420
            except ClientError as ce:
421
                if ce.status in (400,):
422
                    raiseCLIError(
423
                        ce,
424
                        'Nonexistent location for %s' % container_path,
425
                        importance=2, details=this.remote_image_file)
426
                raise
427
        return _raise
428

  
429 399

  
430 400
class pithos(object):
431 401
    container_howto = [
b/kamaki/cli/commands/image.py
57 57
_commands = [image_cmds]
58 58

  
59 59

  
60
about_image_id = [
61
    'To see a list of available image ids: /image list']
60
howto_image_file = [
61
    'Kamaki commands to:',
62
    ' get current user uuid: /user authenticate',
63
    ' check available containers: /file list',
64
    ' create a new container: /file create <container>',
65
    ' check container contents: /file list <container>',
66
    ' upload files: /file upload <image file> <container>']
67

  
68
about_image_id = ['To see a list of available image ids: /image list']
62 69

  
63 70

  
64 71
log = getLogger(__name__)
......
84 91
# Plankton Image Commands
85 92

  
86 93

  
87
def _validate_image_props(json_dict, return_str=False):
94
def _validate_image_meta(json_dict, return_str=False):
88 95
    """
89 96
    :param json_dict" (dict) json-formated, of the form
90 97
        {"key1": "val1", "key2": "val2", ...}
91 98

  
92 99
    :param return_str: (boolean) if true, return a json dump
93 100

  
94
    :returns: (dict)
101
    :returns: (dict) if return_str is not True, else return str
95 102

  
96 103
    :raises TypeError, AttributeError: Invalid json format
97 104

  
......
99 106
    """
100 107
    json_str = dumps(json_dict, indent=2)
101 108
    for k, v in json_dict.items():
102
        dealbreaker = isinstance(v, dict) or isinstance(v, list)
103
        assert not dealbreaker, 'Invalid property value for key %s' % k
104
        dealbreaker = ' ' in k
105
        assert not dealbreaker, 'Invalid key [%s]' % k
109
        if k.lower() == 'properties':
110
            for pk, pv in v.items():
111
                prop_ok = not (isinstance(pv, dict) or isinstance(pv, list))
112
                assert prop_ok, 'Invalid property value for key %s' % pk
113
                key_ok = not (' ' in k or '-' in k)
114
                assert key_ok, 'Invalid property key %s' % k
115
            continue
116
        meta_ok = not (isinstance(v, dict) or isinstance(v, list))
117
        assert meta_ok, 'Invalid value for meta key %s' % k
118
        meta_ok = ' ' not in k
119
        assert meta_ok, 'Invalid meta key [%s]' % k
106 120
        json_dict[k] = '%s' % v
107 121
    return json_str if return_str else json_dict
108 122

  
109 123

  
110
def _load_image_props(filepath):
124
def _load_image_meta(filepath):
111 125
    """
112 126
    :param filepath: (str) the (relative) path of the metafile
113 127

  
......
120 134
    with open(abspath(filepath)) as f:
121 135
        meta_dict = load(f)
122 136
        try:
123
            return _validate_image_props(meta_dict)
137
            return _validate_image_meta(meta_dict)
124 138
        except AssertionError:
125 139
            log.debug('Failed to load properties from file %s' % filepath)
126 140
            raise
127 141

  
128 142

  
143
def _validate_image_location(location):
144
    """
145
    :param location: (str) pithos://<uuid>/<container>/<img-file-path>
146

  
147
    :returns: (<uuid>, <container>, <img-file-path>)
148

  
149
    :raises AssertionError: if location is invalid
150
    """
151
    prefix = 'pithos://'
152
    msg = 'Invalid prefix for location %s , try: %s' % (location, prefix)
153
    assert location.startswith(prefix), msg
154
    service, sep, rest = location.partition('://')
155
    assert sep and rest, 'Location %s is missing uuid' % location
156
    uuid, sep, rest = rest.partition('/')
157
    assert sep and rest, 'Location %s is missing container' % location
158
    container, sep, img_path = rest.partition('/')
159
    assert sep and img_path, 'Location %s is missing image path' % location
160
    return uuid, container, img_path
161

  
162

  
129 163
@command(image_cmds)
130 164
class image_list(_init_image, _optional_json):
131 165
    """List images accessible by user"""
......
244 278
            'set container format',
245 279
            '--container-format'),
246 280
        disk_format=ValueArgument('set disk format', '--disk-format'),
247
        #id=ValueArgument('set image ID', '--id'),
248 281
        owner=ValueArgument('set image owner (admin only)', '--owner'),
249 282
        properties=KeyValueArgument(
250 283
            'add property in key=value form (can be repeated)',
251 284
            ('-p', '--property')),
252 285
        is_public=FlagArgument('mark image as public', '--public'),
253 286
        size=IntArgument('set image size', '--size'),
254
        property_file=ValueArgument(
255
            'Load properties from a json-formated file <img-file>.meta :'
256
            '{"key1": "val1", "key2": "val2", ...}',
257
            ('--property-file')),
258
        prop_file_force=FlagArgument(
259
            'Store remote property object, even it already exists',
260
            ('-f', '--force-upload-property-file')),
261
        no_prop_file_upload=FlagArgument(
262
            'Do not store properties in remote property file',
263
            ('--no-property-file-upload')),
264
        container=ValueArgument(
265
            'Remote image container', ('-C', '--container')),
266
        fileowner=ValueArgument(
267
            'UUID of the user who owns the image file', ('--fileowner'))
287
        metafile=ValueArgument(
288
            'Load metadata from a json-formated file <img-file>.meta :'
289
            '{"key1": "val1", "key2": "val2", ..., "properties: {...}"}',
290
            ('--metafile')),
291
        metafile_force=FlagArgument(
292
            'Store remote metadata object, even if it already exists',
293
            ('-f', '--force')),
294
        no_metafile_upload=FlagArgument(
295
            'Do not store metadata in remote meta file',
296
            ('--no-metafile-upload')),
268 297

  
269 298
    )
270 299

  
271 300
    def _get_uuid(self):
272
        uuid = self['fileowner'] or self.config.get('image', 'fileowner')
273
        if uuid:
274
            return uuid
275 301
        atoken = self.client.token
276 302
        user = AstakosClient(self.config.get('user', 'url'), atoken)
277 303
        return user.term('uuid')
278 304

  
279
    def _get_pithos_client(self, uuid, container):
305
    def _get_pithos_client(self, container):
306
        if self['no_metafile_upload']:
307
            return None
280 308
        purl = self.config.get('file', 'url')
281 309
        ptoken = self.client.token
282
        return PithosClient(purl, ptoken, uuid, container)
310
        return PithosClient(purl, ptoken, self._get_uuid(), container)
283 311

  
284
    def _store_remote_property_file(self, pclient, remote_path, properties):
312
    def _store_remote_metafile(self, pclient, remote_path, metadata):
285 313
        return pclient.upload_from_string(
286
            remote_path, _validate_image_props(properties, return_str=True))
287

  
288
    def _get_container_path(self, container_path):
289
        container = self['container'] or self.config.get('image', 'container')
290
        if container:
291
            return container, container_path
292

  
293
        container, sep, path = container_path.partition(':')
294
        if not sep or not container or not path:
295
            raiseCLIError(
296
                '%s is not a valid pithos+ remote location' % container_path,
297
                importance=2,
298
                details=[
299
                    'To set "image" as container and "my_dir/img.diskdump" as',
300
                    'the image path, try one of the following as '
301
                    'container:path',
302
                    '- <image container>:<remote path>',
303
                    '    e.g. image:/my_dir/img.diskdump',
304
                    '- <remote path> -C <image container>',
305
                    '    e.g. /my_dir/img.diskdump -C image'])
306
        return container, path
314
            remote_path, _validate_image_meta(metadata, return_str=True))
307 315

  
308
    @errors.generic.all
309
    @errors.plankton.image_file
310
    @errors.plankton.connection
311
    def _run(self, name, container_path):
312
        container, path = self._get_container_path(container_path)
313
        uuid = self._get_uuid()
314
        prop_path = '%s.meta' % path
315

  
316
        pclient = None if (
317
            self['no_prop_file_upload']) else self._get_pithos_client(
318
                uuid, container)
319
        if pclient and not self['prop_file_force']:
316
    def _load_params_from_file(self, location):
317
        params, properties = dict(), dict()
318
        pfile = self['metafile']
319
        if pfile:
320 320
            try:
321
                pclient.get_object_info(prop_path)
322
                raiseCLIError('Property file %s: %s already exists' % (
323
                    container, prop_path))
324
            except ClientError as ce:
325
                if ce.status != 404:
326
                    raise
327

  
328
        location = 'pithos://%s/%s/%s' % (uuid, container, path)
321
                for k, v in _load_image_meta(pfile).items():
322
                    key = k.lower().replace('-', '_')
323
                    if k == 'properties':
324
                        for pk, pv in v.items():
325
                            properties[pk.upper().replace('-', '_')] = pv
326
                    elif key == 'name':
327
                            continue
328
                    elif key == 'location':
329
                        if location:
330
                            continue
331
                        location = v
332
                    else:
333
                        params[key] = v
334
            except Exception as e:
335
                raiseCLIError(e, 'Invalid json metadata config file')
336
        return params, properties, location
329 337

  
330
        params = {}
338
    def _load_params_from_args(self, params, properties):
331 339
        for key in set([
332 340
                'checksum',
333 341
                'container_format',
......
336 344
                'size',
337 345
                'is_public']).intersection(self.arguments):
338 346
            params[key] = self[key]
339
        properties = self['properties']
347
        for k, v in self['properties'].items():
348
            properties[k.upper().replace('-', '_')] = v
340 349

  
341
        #load properties
342
        properties = dict()
343
        pfile = self['property_file']
344
        if pfile:
350
    def _validate_location(self, location):
351
        if not location:
352
            raiseCLIError(
353
                'No image file location provided',
354
                importance=2, details=[
355
                    'An image location is needed. Image location format:',
356
                    '  pithos://<uuid>/<container>/<path>',
357
                    ' an image file at the above location must exist.'
358
                    ] + howto_image_file)
359
        try:
360
            return _validate_image_location(location)
361
        except AssertionError as ae:
362
            raiseCLIError(
363
                ae, 'Invalid image location format',
364
                importance=1, details=[
365
                    'Valid image location format:',
366
                    '  pithos://<uuid>/<container>/<img-file-path>'
367
                    ] + howto_image_file)
368

  
369
    @errors.generic.all
370
    @errors.plankton.connection
371
    def _run(self, name, location):
372
        (params, properties, location) = self._load_params_from_file(location)
373
        uuid, container, img_path = self._validate_location(location)
374
        self._load_params_from_args(params, properties)
375
        pclient = self._get_pithos_client(container)
376

  
377
        #check if metafile exists
378
        meta_path = '%s.meta' % img_path
379
        if pclient and not self['metafile_force']:
345 380
            try:
346
                for k, v in _load_image_props(pfile).items():
347
                    properties[k.lower()] = v
348
            except Exception as e:
381
                pclient.get_object_info(meta_path)
382
                raiseCLIError('Metadata file %s:%s already exists' % (
383
                    container, meta_path))
384
            except ClientError as ce:
385
                if ce.status != 404:
386
                    raise
387

  
388
        #register the image
389
        try:
390
            r = self.client.register(name, location, params, properties)
391
        except ClientError as ce:
392
            if ce.status in (400, ):
349 393
                raiseCLIError(
350
                    e, 'Format error in property file %s' % pfile,
394
                    ce, 'Nonexistent image file location %s' % location,
351 395
                    details=[
352
                        'Expected content format:',
353
                        '  {',
354
                        '    "key1": "value1",',
355
                        '    "key2": "value2",',
356
                        '    ...',
357
                        '  }',
358
                        '',
359
                        'Parser:'
360
                    ])
361
        for k, v in self['properties'].items():
362
            properties[k.lower()] = v
363

  
364
        self._print([self.client.register(name, location, params, properties)])
396
                        'Make sure the image file exists'] + howto_image_file)
397
            raise
398
        self._print(r, print_dict)
365 399

  
400
        #upload the metadata file
366 401
        if pclient:
367
            prop_headers = pclient.upload_from_string(
368
                prop_path, _validate_image_props(properties, return_str=True))
402
            try:
403
                meta_headers = pclient.upload_from_string(
404
                    meta_path, dumps(r, indent=2))
405
            except TypeError:
406
                print('Failed to dump metafile %s:%s' % (container, meta_path))
407
                return
369 408
            if self['json_output']:
370 409
                print_json(dict(
371
                    property_file_location='%s:%s' % (container, prop_path),
372
                    headers=prop_headers))
410
                    metafile_location='%s:%s' % (container, meta_path),
411
                    headers=meta_headers))
373 412
            else:
374
                print('Property file uploaded as %s:%s (version %s)' % (
375
                    container, prop_path, prop_headers['x-object-version']))
413
                print('Metadata file uploaded as %s:%s (version %s)' % (
414
                    container, meta_path, meta_headers['x-object-version']))
376 415

  
377
    def main(self, name, container___path):
416
    def main(self, name, location=None):
378 417
        super(self.__class__, self)._run()
379
        self._run(name, container___path)
418
        self._run(name, location)
380 419

  
381 420

  
382 421
@command(image_cmds)

Also available in: Unified diff