2 Showcase: create a virtual cluster from scratch
3 ===============================================
5 In this section we will create a virtual cluster, from scratch.
9 * A `synnefo <http://www.synnefo.org>`_ deployment with functional *Astakos*,
10 *Pithos+*, *Plankton* and *Cyclades* services.
12 * A kamaki setup, configured with a default cloud (see how to do this with
14 `shell command <../examplesdir/configuration.html#multiple-clouds-in-a-single-configuration>`_ ,
16 `python library <config.html#set-a-new-cloud-name-it-new-cloud-and-set-it-as-default>`_.
18 * An image stored at file *./my_image.diskdump* that can run on a predefined
19 hardware flavor, identifiable by the flavor id *42* (see how to create an
21 `synnefo image creator <http://www.synnefo.org/docs/snf-image-creator/latest/index.html>`_
24 This is the pseudocode:
26 #. Get credentials and service endpoints, with kamaki config and the
27 **Astakos** *identity* and *account* services
28 #. Upload the image file to the **Pithos+** *object-store* service
29 #. Register the image file to the **Plankton** *image* service
30 #. Create a number of virtual servers to the **Cyclades** *compute* service
33 Credentials and endpoints
34 -------------------------
36 We assume that the kamaki configuration file contains at least one cloud
37 configuration, and this configuration is also set as the default cloud for
38 kamaki. A cloud configuration is basically a name for the cloud, an
39 authentication URL and an authentication TOKEN: the credentials we are looking
44 #. Get the credentials from the kamaki configuration
45 #. Initialize an AstakosClient and test the credentials
46 #. Get the endpoints for all services
48 .. code-block:: python
50 from sys import stderr
51 from kamaki.cli.config import Config, CONFIG_PATH
52 from kamaki.clients.astakos import AstakosClient, ClientError
54 # Initialize Config with default values.
57 # 1. Get the credentials
58 # Get default cloud name
60 cloud_name = cnf.get('global', 'default_cloud')
62 stderr.write('No default cloud set in file %s\n' % CONFIG_PATH)
65 # Get cloud authentication URL and TOKEN
67 AUTH_URL = cnf.get_cloud(cloud_name, 'url')
69 stderr.write('No authentication URL in cloud %s\n' % cloud_name)
72 AUTH_TOKEN = cnf.get_cloud(cloud_name, 'token')
74 stderr.write('No token in cloud %s\n' % cloud_name)
77 # 2. Test the credentials
78 # Test authentication credentials
80 auth = AstakosClient(AUTH_URL, AUTH_TOKEN)
83 stderr.write('Athentication failed with url %s and token %s\n' % (
84 AUTH_URL, AUTH_TOKEN))
87 # 3. Get the endpoints
88 # Identity, Account --> astakos
89 # Compute --> cyclades
90 # Object-store --> pithos
95 cyclades=auth.get_service_endpoints('compute')['publicURL'],
96 pithos=auth.get_service_endpoints('object-store')['publicURL'],
97 plankton=auth.get_service_endpoints('image')['publicURL']
99 user_id = auth.user_info()['id']
102 'Failed to get user id and endpoints from the identity server\n')
108 We assume there is an image file at the current local directory, at
109 *./my_image.diskdump* and we need to upload it to a Pithos+ container. We also
110 assume the contains does not currently exist. We will name it *images*.
114 #. Initialize a Pithos+ client
115 #. Create the container *images*
116 #. Upload the local file to the container
118 .. code-block:: python
120 from os.path import abspath
121 from kamaki.clients.pithos import PithosClient
124 IMAGE_FILE = 'my_image.diskdump'
126 # 1. Initialize Pithos+ client and set account to current user
128 pithos = PithosClient(endpoints['pithos'], AUTH_TOKEN)
130 stderr.write('Failed to initialize a Pithos+ client\n')
132 pithos.account = user_id
134 # 2. Create the container "images" and let pithos client work with that
136 pithos.create_container('images')
138 stderr.write('Failed to create container "image"\n')
140 pithos.container = CONTAINER
143 with open(abspath(IMAGE_FILE)) as f:
145 pithos.upload_object(IMAGE_FILE, f)
147 stderr.write('Failed to upload file %s to container %s\n' % (
148 IMAGE_FILE, CONTAINER))
154 Now the image is located at *pithos://<user_id>/images/my_image.diskdump*
155 and we want to register it to the Plankton *image* service.
157 .. code-block:: python
159 from kamaki.clients.image import ImageClient
161 IMAGE_NAME = 'My image'
162 IMAGE_LOCATION = (user_id, CONTAINER, IMAGE_FILE)
164 # 3.1 Initialize ImageClient
166 plankton = ImageClient(endpoints['plankton'], AUTH_TOKEN)
168 stderr.write('Failed to initialize the Image client client\n')
171 # 3.2 Register the image
172 properties = dict(osfamily='linux', root_partition='1')
174 image = plankton.image_register(IMAGE_NAME, IMAGE_LOCATION)
176 stderr.write('Failed to register image %s\n' % IMAGE_NAME)
179 Create the virtual cluster
180 --------------------------
182 In order to build a virtual cluster, we need some information:
184 * an image id. We can get them from *image['id']* (the id of the image we
186 * a hardware flavor. Assume we have picked the flavor with id *42*
187 * a set of names for our virtual servers. We will name them *cluster1*,
192 #. Initialize a Cyclades/Compute client
193 #. Create a number of virtual servers. Their name should be prefixed as
196 .. code-block:: python
198 # 4. Create virtual cluster
199 from kamaki.clients.cyclades import CycladesClient
202 IMAGE_ID = image['id']
204 CLUSTER_PREFIX = 'cluster'
206 # 4.1 Initialize a cyclades client
208 cyclades = CycladesClient(endpoints['cyclades'], AUTH_TOKEN)
210 stderr.write('Failed to initialize cyclades client\n')
213 # 4.2 Create 2 servers prefixed as "cluster"
215 for i in range(1, CLUSTER_SIZE + 1):
216 server_name = '%s%s' % (CLUSTER_PREFIX, i)
219 cyclades.create_server(server_name, FLAVOR_ID, IMAGE_ID))
221 stderr.write('Failed while creating server %s\n' % server_name)
230 Uploading an image might take a while. You can wait patiently, or you can use a
231 progress generator. Even better, combine a generator with the progress bar
232 package that comes with kamaki. The upload_object method accepts two generators
233 as parameters: one for calculating local file hashes and another for uploading
235 .. code-block:: python
237 from progress.bar import Bar
240 bar = Bar('Calculating hashes...')
241 for i in bar.iter(range(int(n))):
246 bar = Bar('Uploading...')
247 for i in bar.iter(range(int(n))):
252 pithos.upload_object(
253 IMAGE_FILE, f, hash_cb=hash_gen, upload_cb=upload_gen)
255 We can create a method to produce progress bar generators, and use it in other
258 .. code-block:: python
261 from progress.bar import Bar
266 for i in bar.iter(range(int(n))):
271 stderr.write('Suggestion: install python-progress\n')
276 pithos.upload_object(
278 hash_cb=create_pb('Calculating hashes...'),
279 upload_cb=create_pb('Uploading...'))
281 Wait for servers to built
282 '''''''''''''''''''''''''
284 When a create_server method is finished successfully, a server is being built.
285 Usually, it takes a while for a server to built. Fortunately, there is a wait
286 method in the kamaki cyclades client. It can use a progress bar too!
288 .. code-block:: python
290 # 4.2 Create 2 servers prefixed as "cluster"
293 # 4.3 Wait for servers to built
294 for server in servers:
295 cyclades.wait_server(server['id'])
297 Asynchronous server creation
298 ''''''''''''''''''''''''''''
300 In case of a large virtual cluster, it might be faster to spawn the servers
301 with asynchronous requests. Kamaki clients offer an automated mechanism for
302 asynchronous requests.
304 .. code-block:: python
306 # 4.2 Create 2 servers prefixed as "cluster"
307 create_params = [dict(
308 name='%s%s' % (CLUSTER_PREFIX, i),
310 image_id=IMAGE_ID) for i in range(1, CLUSTER_SIZE + 1)]
312 servers = cyclades.async_run(cyclades.create_server, create_params)
314 stderr.write('Failed while creating servers\n')
317 Clean up virtual cluster
318 ''''''''''''''''''''''''
320 We need to clean up Cyclades from servers left from previous cluster creations.
321 This clean up will destroy all servers prefixed with "cluster". It will run
322 before the cluster creation:
324 .. code-block:: python
326 # 4.2 Clean up virtual cluster
327 to_delete = [server for server in cyclades.list_servers(detail=True) if (
328 server['name'].startswith(CLUSTER_PREFIX))]
329 for server in to_delete:
330 cyclades.delete_server(server['id'])
331 for server in to_delete:
332 cyclades.wait_server(
333 server['id'], server['status'],
334 wait_cb=create_pb('Deleting %s...' % server['name']))
336 # 4.3 Create 2 servers prefixed as "cluster"
342 When a server is created, the returned value contains a filed "adminPass". This
343 field can be used to manually log into the server.
346 `inject the ssh keys <../examplesdir/server.html#inject-ssh-keys-to-a-debian-server>`_
347 of the users who are going to use the virtual servers.
349 Assuming that we have collected the keys in a file named *rsa.pub*, we can
350 inject them into each server, with the personality argument
352 .. code-block:: python
358 # 4.3 Create 2 servers prefixed as "cluster"
361 with open(abspath(SSH_KEYS)) as f:
362 personality.append(dict(
363 contents=b64encode(f.read()),
364 path='/root/.ssh/authorized_keys',
365 owner='root', group='root', mode='0600')
366 personality.append(dict(
367 contents=b64encode('StrictHostKeyChecking no'),
368 path='/root/.ssh/config',
369 owner='root', group='root', mode='0600'))
371 create_params = [dict(
372 name='%s%s' % (CLUSTER_PREFIX, i),
375 personality=personality) for i in range(1, CLUSTER_SIZE + 1)]
378 Save server passwords in a file
379 '''''''''''''''''''''''''''''''
381 A last touch: define a local file to store the created server information,
382 including the superuser password.
384 .. code-block:: python
386 # 4.4 Store passwords in file
387 SERVER_INFO = 'servers.txt'
388 with open(abspath(SERVER_INFO), 'w+') as f:
389 from json import dump
390 dump(servers, f, intend=2)
392 # 4.5 Wait for 2 servers to built
398 Developers may use the kamaki tools for
399 `error handling <clients-api.html#error-handling>`_ and
400 `logging <logging.html>`_, or implement their own methods.
402 To demonstrate, we will modify the container creation code to warn users if the
403 container already exists. We need a stream logger for the warning and a
404 knowledge of the expected return values for the *create_container* method.
406 First, let's get the logger.
408 .. code-block:: python
410 from kamaki.cli.logger import add_stream_logger, get_logger
412 add_stream_logger(__name__)
413 log = get_logger(__name__)
415 The *create_container* method makes an HTTP request to the pithos server. It
416 considers the request succesfull if the status code of the response is 201
417 (created) or 202 (accepted). These status codes mean that the container has
418 been created or that it was already there anyway, respectively.
420 We will force *create_container* to raise an error in case of a 202 response.
421 This can be done by instructing *create_container* to accept only 201 as a
424 .. code-block:: python
427 pithos.create_container(CONTAINER, success=(201, ))
428 except ClientError as ce:
429 if ce.status in (202, ):
430 log.warning('Container %s already exists' % CONTAINER')
432 log.debug('Failed to create container %s' % CONTAINER)
434 log.info('Container %s is ready' % CONTAINER)
436 create a cluster from scratch
437 -----------------------------
439 We are ready to create a module that uses kamaki to create a cluster from
440 scratch. We revised the code by grouping functionality in methods and using
441 logging more. We also added some command line interaction candy.
443 .. code-block:: python
445 #!/usr/bin/env python
448 from os.path import abspath
449 from base64 import b64encode
450 from kamaki.clients import ClientError
451 from kamaki.cli.logger import get_logger, add_file_logger
452 from logging import DEBUG
455 log = get_logger(__name__)
456 add_file_logger('kamaki.clients', DEBUG, '%s.log' % __name__)
457 add_file_logger(__name__, DEBUG, '%s.log' % __name__)
459 # Create progress bar generator
461 from progress.bar import Bar
466 for i in bar.iter(range(int(n))):
471 log.warning('Suggestion: install python-progress')
477 # Identity,Account / Astakos
480 from kamaki.clients.astakos import AstakosClient
481 from kamaki.cli.config import Config, CONFIG_PATH
483 print(' Get the credentials')
486 # Get default cloud name
488 cloud_name = cnf.get('global', 'default_cloud')
490 log.debug('No default cloud set in file %' % CONFIG_PATH)
494 AUTH_URL = cnf.get_cloud(cloud_name, 'url')
496 log.debug('No authentication URL in cloud %s' % cloud_name)
499 AUTH_TOKEN = cnf.get_cloud(cloud_name, 'token')
501 log.debug('No token in cloud %s' % cloud_name)
504 print(' Test the credentials')
506 auth = AstakosClient(AUTH_URL, AUTH_TOKEN)
509 log.debug('Athentication failed with url %s and token %s' % (
510 AUTH_URL, AUTH_TOKEN))
513 return auth, AUTH_TOKEN
516 def endpoints_and_user_id(auth):
517 print(' Get the endpoints')
520 astakos=auth.get_service_endpoints('identity')['publicURL'],
521 cyclades=auth.get_service_endpoints('compute')['publicURL'],
522 pithos=auth.get_service_endpoints('object-store')['publicURL'],
523 plankton=auth.get_service_endpoints('image')['publicURL']
525 user_id = auth.user_info()['id']
527 print('Failed to get endpoints & user_id from identity server')
529 return endpoints, user_id
532 # Object-store / Pithos+
534 def init_pithos(endpoint, token, user_id):
535 from kamaki.clients.pithos import PithosClient
537 print(' Initialize Pithos+ client and set account to user uuid')
539 return PithosClient(endpoint, token, user_id)
541 log.debug('Failed to initialize a Pithos+ client')
545 def upload_image(pithos, container, image_path):
547 print(' Create the container "images" and use it')
549 pithos.create_container(container, success=(201, ))
550 except ClientError as ce:
551 if ce.status in (202, ):
552 log.warning('Container %s already exists' % container)
554 log.debug('Failed to create container %s' % container)
556 pithos.container = container
558 print(' Upload to "images"')
559 with open(abspath(image_path)) as f:
561 pithos.upload_object(
563 hash_cb=create_pb(' Calculating hashes...'),
564 upload_cb=create_pb(' Uploading...'))
566 log.debug('Failed to upload file %s to container %s' % (
567 image_path, container))
573 def init_plankton(endpoint, token):
574 from kamaki.clients.image import ImageClient
576 print(' Initialize ImageClient')
578 return ImageClient(endpoint, token)
580 log.debug('Failed to initialize the Image client')
584 def register_image(plankton, name, user_id, container, path, properties):
586 image_location = (user_id, container, path)
587 print(' Register the image')
589 return plankton.register(name, image_location, properties)
591 log.debug('Failed to register image %s' % name)
597 def init_cyclades(endpoint, token):
598 from kamaki.clients.cyclades import CycladesClient
600 print(' Initialize a cyclades client')
602 return CycladesClient(endpoint, token)
604 log.debug('Failed to initialize cyclades client')
608 class Cluster(object):
610 def __init__(self, cyclades, prefix, flavor_id, image_id, size):
611 self.client = cyclades
612 self.prefix, self.size = prefix, int(size)
613 self.flavor_id, self.image_id = flavor_id, image_id
616 return [s for s in self.client.list_servers(detail=True) if (
617 s['name'].startswith(self.prefix))]
620 to_delete = self.list()
621 print(' There are %s servers to clean up' % len(to_delete))
622 for server in to_delete:
623 self.client.delete_server(server['id'])
624 for server in to_delete:
625 self.client.wait_server(
626 server['id'], server['status'],
627 wait_cb=create_pb(' Deleting %s...' % server['name']))
629 def _personality(self, ssh_keys_path='', pub_keys_path=''):
632 with open(abspath(ssh_keys_path)) as f:
633 personality.append(dict(
634 contents=b64encode(f.read()),
635 path='/root/.ssh/id_rsa',
636 owner='root', group='root', mode='0600'))
638 with open(abspath(pub_keys_path)) as f:
639 personality.append(dict(
640 contents=b64encode(f.read()),
641 path='/root/.ssh/authorized_keys',
642 owner='root', group='root', mode='0600'))
643 if ssh_keys_path or pub_keys_path:
644 personality.append(dict(
645 contents=b64encode('StrictHostKeyChecking no'),
646 path='/root/.ssh/config',
647 owner='root', group='root', mode='0600'))
650 def create(self, ssh_k_path='', pub_k_path='', server_log_path=''):
651 print('\n Create %s servers prefixed as %s' % (
652 self.size, self.prefix))
654 for i in range(1, self.size + 1):
656 server_name = '%s%s' % (self.prefix, i)
657 servers.append(self.client.create_server(
658 server_name, self.flavor_id, self.image_id,
659 personality=self._personality(ssh_k_path, pub_k_path)))
661 log.debug('Failed while creating server %s' % server_name)
665 print(' Store passwords in file %s' % server_log_path)
666 with open(abspath(server_log_path), 'w+') as f:
667 from json import dump
668 dump(servers, f, indent=2)
670 print(' Wait for %s servers to built' % self.size)
671 for server in servers:
672 new_status = self.client.wait_server(
674 wait_cb=create_pb(' Creating %s...' % server['name']))
675 print(' Status for server %s is %s' % (
676 server['name'], new_status or 'not changed yet'))
682 print('1. Credentials and Endpoints')
683 auth, token = init_astakos()
684 endpoints, user_id = endpoints_and_user_id(auth)
686 print('2. Upload the image file')
687 pithos = init_pithos(endpoints['pithos'], token, user_id)
689 upload_image(pithos, opts.container, opts.imagefile)
691 print('3. Register the image')
692 plankton = init_plankton(endpoints['plankton'], token)
694 image = register_image(
695 plankton, 'my image', user_id, opts.container, opts.imagefile,
697 osfamily=opts.osfamily, root_partition=opts.rootpartition))
699 print('4. Create virtual cluster')
701 cyclades = init_cyclades(endpoints['cyclades'], token),
703 flavor_id=opts.flavorid,
704 image_id=image['id'],
705 size=opts.clustersize)
706 if opts.delete_stale:
708 servers = cluster.create(
709 opts.sshkeypath, opts.pubkeypath, opts.serverlogpath)
712 cluster_servers = cluster.list()
714 active = [s for s in cluster_servers if s['status'] == 'ACTIVE']
715 print('%s cluster servers are ACTIVE' % len(active))
717 attached = [s for s in cluster_servers if s['attachments']]
718 print('%s cluster servers are attached to networks' % len(attached))
720 build = [s for s in cluster_servers if s['status'] == 'BUILD']
721 print('%s cluster servers are being built' % len(build))
723 error = [s for s in cluster_servers if s['status'] in ('ERROR')]
724 print('%s cluster servers failed (ERROR satus)' % len(error))
727 if __name__ == '__main__':
729 # Add some interaction candy
730 from optparse import OptionParser
733 kw['usage'] = '%prog [options]'
734 kw['description'] = '%prog deploys a compute cluster on Synnefo w. kamaki'
736 parser = OptionParser(**kw)
737 parser.disable_interspersed_args()
738 parser.add_option('--prefix',
739 action='store', type='string', dest='prefix',
740 help='The prefix to use for naming cluster nodes',
742 parser.add_option('--clustersize',
743 action='store', type='string', dest='clustersize',
744 help='Number of virtual cluster nodes to create ',
746 parser.add_option('--flavor-id',
747 action='store', type='int', dest='flavorid',
749 help='Choose flavor id for the virtual hardware '
752 parser.add_option('--image-file',
753 action='store', type='string', dest='imagefile',
754 metavar='IMAGE FILE PATH',
755 help='The image file to upload and register ',
756 default='my_image.diskdump')
757 parser.add_option('--delete-stale',
758 action='store_true', dest='delete_stale',
759 help='Delete stale servers from previous runs, whose '
760 'name starts with the specified prefix, see '
763 parser.add_option('--container',
764 action='store', type='string', dest='container',
765 metavar='PITHOS+ CONTAINER',
766 help='The Pithos+ container to store image file',
768 parser.add_option('--ssh-key-path',
769 action='store', type='string', dest='sshkeypath',
770 metavar='PATH OF SSH KEYS',
771 help='The ssh keys to inject to server (e.g., id_rsa) ',
773 parser.add_option('--pub-key-path',
774 action='store', type='string', dest='pubkeypath',
775 metavar='PATH OF PUBLIC KEYS',
776 help='The public keys to inject to server',
778 parser.add_option('--server-log-path',
779 action='store', type='string', dest='serverlogpath',
780 metavar='FILE TO LOG THE VIRTUAL SERVERS',
781 help='Where to store information on created servers '
782 'including superuser passwords',
784 parser.add_option('--image-osfamily',
785 action='store', type='string', dest='osfamily',
787 help='linux, windows, etc.',
789 parser.add_option('--image-root-partition',
790 action='store', type='string', dest='rootpartition',
791 metavar='IMAGE ROOT PARTITION',
792 help='The partition where the root home is ',
795 opts, args = parser.parse_args(argv[1:])