Second patch for volume support.
[snaps.git] / snaps / openstack / utils / nova_utils.py
1 # Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
2 #                    and others.  All rights reserved.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at:
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15
16 import logging
17
18 import os
19 from cryptography.hazmat.backends import default_backend
20 from cryptography.hazmat.primitives import serialization
21 from cryptography.hazmat.primitives.asymmetric import rsa
22 from novaclient.client import Client
23 from novaclient.exceptions import NotFound
24
25 from snaps import file_utils
26 from snaps.domain.flavor import Flavor
27 from snaps.domain.keypair import Keypair
28 from snaps.domain.project import ComputeQuotas
29 from snaps.domain.vm_inst import VmInst
30 from snaps.openstack.utils import keystone_utils, glance_utils, neutron_utils
31
32 __author__ = 'spisarski'
33
34 logger = logging.getLogger('nova_utils')
35
36 """
37 Utilities for basic OpenStack Nova API calls
38 """
39
40
41 def nova_client(os_creds):
42     """
43     Instantiates and returns a client for communications with OpenStack's Nova
44     server
45     :param os_creds: The connection credentials to the OpenStack API
46     :return: the client object
47     """
48     logger.debug('Retrieving Nova Client')
49     return Client(os_creds.compute_api_version,
50                   session=keystone_utils.keystone_session(os_creds),
51                   region_name=os_creds.region_name)
52
53
54 def create_server(nova, neutron, glance, instance_settings, image_settings,
55                   keypair_settings=None):
56     """
57     Creates a VM instance
58     :param nova: the nova client (required)
59     :param neutron: the neutron client for retrieving ports (required)
60     :param glance: the glance client (required)
61     :param instance_settings: the VM instance settings object (required)
62     :param image_settings: the VM's image settings object (required)
63     :param keypair_settings: the VM's keypair settings object (optional)
64     :return: a snaps.domain.VmInst object
65     """
66
67     ports = list()
68
69     for port_setting in instance_settings.port_settings:
70         ports.append(neutron_utils.get_port(
71             neutron, port_settings=port_setting))
72     nics = []
73     for port in ports:
74         kv = dict()
75         kv['port-id'] = port.id
76         nics.append(kv)
77
78     logger.info('Creating VM with name - ' + instance_settings.name)
79     keypair_name = None
80     if keypair_settings:
81         keypair_name = keypair_settings.name
82
83     flavor = get_flavor_by_name(nova, instance_settings.flavor)
84     if not flavor:
85         raise NovaException(
86             'Flavor not found with name - %s', instance_settings.flavor)
87
88     image = glance_utils.get_image(glance, image_settings=image_settings)
89     if image:
90         userdata = None
91         if instance_settings.userdata:
92             if isinstance(instance_settings.userdata, str):
93                 userdata = instance_settings.userdata + '\n'
94             elif (isinstance(instance_settings.userdata, dict) and
95                   'script_file' in instance_settings.userdata):
96                 try:
97                     userdata = file_utils.read_file(
98                         instance_settings.userdata['script_file'])
99                 except Exception as e:
100                     logger.warn('error reading userdata file %s - %s',
101                                 instance_settings.userdata, e)
102         args = {'name': instance_settings.name,
103                 'flavor': flavor,
104                 'image': image,
105                 'nics': nics,
106                 'key_name': keypair_name,
107                 'security_groups':
108                     instance_settings.security_group_names,
109                 'userdata': userdata}
110
111         if instance_settings.availability_zone:
112             args['availability_zone'] = instance_settings.availability_zone
113
114         server = nova.servers.create(**args)
115
116         return __map_os_server_obj_to_vm_inst(server)
117     else:
118         raise NovaException(
119             'Cannot create instance, image cannot be located with name %s',
120             image_settings.name)
121
122
123 def get_server(nova, vm_inst_settings=None, server_name=None):
124     """
125     Returns a VmInst object for the first server instance found.
126     :param nova: the Nova client
127     :param vm_inst_settings: the VmInstanceSettings object from which to build
128                              the query if not None
129     :param server_name: the server with this name to return if vm_inst_settings
130                         is not None
131     :return: a snaps.domain.VmInst object or None if not found
132     """
133     search_opts = dict()
134     if vm_inst_settings:
135         search_opts['name'] = vm_inst_settings.name
136     elif server_name:
137         search_opts['name'] = server_name
138
139     servers = nova.servers.list(search_opts=search_opts)
140     for server in servers:
141         return __map_os_server_obj_to_vm_inst(server)
142
143
144 def __map_os_server_obj_to_vm_inst(os_server):
145     """
146     Returns a VmInst object for an OpenStack Server object
147     :param os_server: the OpenStack server object
148     :return: an equivalent SNAPS-OO VmInst domain object
149     """
150     sec_grp_names = list()
151     # VM must be active for 'security_groups' attr to be initialized
152     if hasattr(os_server, 'security_groups'):
153         for sec_group in os_server.security_groups:
154             if sec_group.get('name'):
155                 sec_grp_names.append(sec_group.get('name'))
156
157     return VmInst(
158         name=os_server.name, inst_id=os_server.id,
159         image_id=os_server.image['id'], flavor_id=os_server.flavor['id'],
160         networks=os_server.networks, keypair_name=os_server.key_name,
161         sec_grp_names=sec_grp_names)
162
163
164 def __get_latest_server_os_object(nova, server):
165     """
166     Returns a server with a given id
167     :param nova: the Nova client
168     :param server: the domain VmInst object
169     :return: the list of servers or None if not found
170     """
171     return __get_latest_server_os_object_by_id(nova, server.id)
172
173
174 def __get_latest_server_os_object_by_id(nova, server_id):
175     """
176     Returns a server with a given id
177     :param nova: the Nova client
178     :param server_id: the server's ID
179     :return: the list of servers or None if not found
180     """
181     return nova.servers.get(server_id)
182
183
184 def get_server_status(nova, server):
185     """
186     Returns the a VM instance's status from OpenStack
187     :param nova: the Nova client
188     :param server: the domain VmInst object
189     :return: the VM's string status or None if not founc
190     """
191     server = __get_latest_server_os_object(nova, server)
192     if server:
193         return server.status
194     return None
195
196
197 def get_server_console_output(nova, server):
198     """
199     Returns the console object for parsing VM activity
200     :param nova: the Nova client
201     :param server: the domain VmInst object
202     :return: the console output object or None if server object is not found
203     """
204     server = __get_latest_server_os_object(nova, server)
205     if server:
206         return server.get_console_output()
207     return None
208
209
210 def get_latest_server_object(nova, server):
211     """
212     Returns a server with a given id
213     :param nova: the Nova client
214     :param server: the old server object
215     :return: the list of servers or None if not found
216     """
217     server = __get_latest_server_os_object(nova, server)
218     return __map_os_server_obj_to_vm_inst(server)
219
220
221 def get_server_object_by_id(nova, server_id):
222     """
223     Returns a server with a given id
224     :param nova: the Nova client
225     :param server_id: the server's id
226     :return: an SNAPS-OO VmInst object or None if not found
227     """
228     server = __get_latest_server_os_object_by_id(nova, server_id)
229     return __map_os_server_obj_to_vm_inst(server)
230
231
232 def get_server_security_group_names(nova, server):
233     """
234     Returns a server with a given id
235     :param nova: the Nova client
236     :param server: the old server object
237     :return: the list of security groups associated with a VM
238     """
239     out = list()
240     os_vm_inst = __get_latest_server_os_object(nova, server)
241     for sec_grp_dict in os_vm_inst.security_groups:
242         out.append(sec_grp_dict['name'])
243     return out
244
245
246 def get_server_info(nova, server):
247     """
248     Returns a dictionary of a VMs info as returned by OpenStack
249     :param nova: the Nova client
250     :param server: the old server object
251     :return: a dict of the info if VM exists else None
252     """
253     vm = __get_latest_server_os_object(nova, server)
254     if vm:
255         return vm._info
256     return None
257
258
259 def create_keys(key_size=2048):
260     """
261     Generates public and private keys
262     :param key_size: the number of bytes for the key size
263     :return: the cryptography keys
264     """
265     return rsa.generate_private_key(backend=default_backend(),
266                                     public_exponent=65537,
267                                     key_size=key_size)
268
269
270 def public_key_openssh(keys):
271     """
272     Returns the public key for OpenSSH
273     :param keys: the keys generated by create_keys() from cryptography
274     :return: the OpenSSH public key
275     """
276     return keys.public_key().public_bytes(serialization.Encoding.OpenSSH,
277                                           serialization.PublicFormat.OpenSSH)
278
279
280 def save_keys_to_files(keys=None, pub_file_path=None, priv_file_path=None):
281     """
282     Saves the generated RSA generated keys to the filesystem
283     :param keys: the keys to save generated by cryptography
284     :param pub_file_path: the path to the public keys
285     :param priv_file_path: the path to the private keys
286     """
287     if keys:
288         if pub_file_path:
289             # To support '~'
290             pub_expand_file = os.path.expanduser(pub_file_path)
291             pub_dir = os.path.dirname(pub_expand_file)
292
293             if not os.path.isdir(pub_dir):
294                 os.mkdir(pub_dir)
295
296             public_handle = None
297             try:
298                 public_handle = open(pub_expand_file, 'wb')
299                 public_bytes = keys.public_key().public_bytes(
300                     serialization.Encoding.OpenSSH,
301                     serialization.PublicFormat.OpenSSH)
302                 public_handle.write(public_bytes)
303             finally:
304                 if public_handle:
305                     public_handle.close()
306
307             os.chmod(pub_expand_file, 0o600)
308             logger.info("Saved public key to - " + pub_expand_file)
309         if priv_file_path:
310             # To support '~'
311             priv_expand_file = os.path.expanduser(priv_file_path)
312             priv_dir = os.path.dirname(priv_expand_file)
313             if not os.path.isdir(priv_dir):
314                 os.mkdir(priv_dir)
315
316             private_handle = None
317             try:
318                 private_handle = open(priv_expand_file, 'wb')
319                 private_handle.write(
320                     keys.private_bytes(
321                         encoding=serialization.Encoding.PEM,
322                         format=serialization.PrivateFormat.TraditionalOpenSSL,
323                         encryption_algorithm=serialization.NoEncryption()))
324             finally:
325                 if private_handle:
326                     private_handle.close()
327
328             os.chmod(priv_expand_file, 0o600)
329             logger.info("Saved private key to - " + priv_expand_file)
330
331
332 def upload_keypair_file(nova, name, file_path):
333     """
334     Uploads a public key from a file
335     :param nova: the Nova client
336     :param name: the keypair name
337     :param file_path: the path to the public key file
338     :return: the keypair object
339     """
340     fpubkey = None
341     try:
342         with open(os.path.expanduser(file_path), 'rb') as fpubkey:
343             logger.info('Saving keypair to - ' + file_path)
344             return upload_keypair(nova, name, fpubkey.read())
345     finally:
346         if fpubkey:
347             fpubkey.close()
348
349
350 def upload_keypair(nova, name, key):
351     """
352     Uploads a public key from a file
353     :param nova: the Nova client
354     :param name: the keypair name
355     :param key: the public key object
356     :return: the keypair object
357     """
358     logger.info('Creating keypair with name - ' + name)
359     os_kp = nova.keypairs.create(name=name, public_key=key.decode('utf-8'))
360     return Keypair(name=os_kp.name, kp_id=os_kp.id,
361                    public_key=os_kp.public_key, fingerprint=os_kp.fingerprint)
362
363
364 def keypair_exists(nova, keypair_obj):
365     """
366     Returns a copy of the keypair object if found
367     :param nova: the Nova client
368     :param keypair_obj: the keypair object
369     :return: the keypair object or None if not found
370     """
371     try:
372         os_kp = nova.keypairs.get(keypair_obj)
373         return Keypair(name=os_kp.name, kp_id=os_kp.id,
374                        public_key=os_kp.public_key)
375     except:
376         return None
377
378
379 def get_keypair_by_name(nova, name):
380     """
381     Returns a list of all available keypairs
382     :param nova: the Nova client
383     :param name: the name of the keypair to lookup
384     :return: the keypair object or None if not found
385     """
386     keypairs = nova.keypairs.list()
387
388     for keypair in keypairs:
389         if keypair.name == name:
390             return Keypair(name=keypair.name, kp_id=keypair.id,
391                            public_key=keypair.public_key)
392
393     return None
394
395
396 def delete_keypair(nova, key):
397     """
398     Deletes a keypair object from OpenStack
399     :param nova: the Nova client
400     :param key: the SNAPS-OO keypair domain object to delete
401     """
402     logger.debug('Deleting keypair - ' + key.name)
403     nova.keypairs.delete(key.id)
404
405
406 def get_availability_zone_hosts(nova, zone_name='nova'):
407     """
408     Returns the names of all nova active compute servers
409     :param nova: the Nova client
410     :param zone_name: the Nova client
411     :return: a list of compute server names
412     """
413     out = list()
414     zones = nova.availability_zones.list()
415     for zone in zones:
416         if zone.zoneName == zone_name and zone.hosts:
417             for key, host in zone.hosts.items():
418                 if host['nova-compute']['available']:
419                     out.append(zone.zoneName + ':' + key)
420
421     return out
422
423
424 def delete_vm_instance(nova, vm_inst):
425     """
426     Deletes a VM instance
427     :param nova: the nova client
428     :param vm_inst: the snaps.domain.VmInst object
429     """
430     nova.servers.delete(vm_inst.id)
431
432
433 def __get_os_flavor(nova, flavor_id):
434     """
435     Returns to OpenStack flavor object by name
436     :param nova: the Nova client
437     :param flavor_id: the flavor's ID value
438     :return: the OpenStack Flavor object
439     """
440     try:
441         return nova.flavors.get(flavor_id)
442     except NotFound:
443         return None
444
445
446 def get_flavor(nova, flavor):
447     """
448     Returns to OpenStack flavor object by name
449     :param nova: the Nova client
450     :param flavor: the SNAPS flavor domain object
451     :return: the SNAPS Flavor domain object
452     """
453     os_flavor = __get_os_flavor(nova, flavor.id)
454     if os_flavor:
455         return Flavor(
456             name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
457             disk=os_flavor.disk, vcpus=os_flavor.vcpus,
458             ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
459             rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
460     try:
461         return nova.flavors.get(flavor.id)
462     except NotFound:
463         return None
464
465
466 def get_flavor_by_id(nova, flavor_id):
467     """
468     Returns to OpenStack flavor object by name
469     :param nova: the Nova client
470     :param flavor_id: the flavor ID value
471     :return: the SNAPS Flavor domain object
472     """
473     os_flavor = __get_os_flavor(nova, flavor_id)
474     if os_flavor:
475         return Flavor(
476             name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
477             disk=os_flavor.disk, vcpus=os_flavor.vcpus,
478             ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
479             rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
480
481
482 def __get_os_flavor_by_name(nova, name):
483     """
484     Returns to OpenStack flavor object by name
485     :param nova: the Nova client
486     :param name: the name of the flavor to query
487     :return: OpenStack flavor object
488     """
489     try:
490         return nova.flavors.find(name=name)
491     except NotFound:
492         return None
493
494
495 def get_flavor_by_name(nova, name):
496     """
497     Returns a flavor by name
498     :param nova: the Nova client
499     :param name: the flavor name to return
500     :return: the SNAPS flavor domain object or None if not exists
501     """
502     os_flavor = __get_os_flavor_by_name(nova, name)
503     if os_flavor:
504         return Flavor(
505             name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
506             disk=os_flavor.disk, vcpus=os_flavor.vcpus,
507             ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
508             rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
509
510
511 def create_flavor(nova, flavor_settings):
512     """
513     Creates and returns and OpenStack flavor object
514     :param nova: the Nova client
515     :param flavor_settings: the flavor settings
516     :return: the SNAPS flavor domain object
517     """
518     os_flavor = nova.flavors.create(
519         name=flavor_settings.name, flavorid=flavor_settings.flavor_id,
520         ram=flavor_settings.ram, vcpus=flavor_settings.vcpus,
521         disk=flavor_settings.disk, ephemeral=flavor_settings.ephemeral,
522         swap=flavor_settings.swap, rxtx_factor=flavor_settings.rxtx_factor,
523         is_public=flavor_settings.is_public)
524     return Flavor(
525         name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
526         disk=os_flavor.disk, vcpus=os_flavor.vcpus,
527         ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
528         rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
529
530
531 def delete_flavor(nova, flavor):
532     """
533     Deletes a flavor
534     :param nova: the Nova client
535     :param flavor: the SNAPS flavor domain object
536     """
537     nova.flavors.delete(flavor.id)
538
539
540 def set_flavor_keys(nova, flavor, metadata):
541     """
542     Sets metadata on the flavor
543     :param nova: the Nova client
544     :param flavor: the SNAPS flavor domain object
545     :param metadata: the metadata to set
546     """
547     os_flavor = __get_os_flavor(nova, flavor.id)
548     if os_flavor:
549         os_flavor.set_keys(metadata)
550
551
552 def get_flavor_keys(nova, flavor):
553     """
554     Sets metadata on the flavor
555     :param nova: the Nova client
556     :param flavor: the SNAPS flavor domain object
557     """
558     os_flavor = __get_os_flavor(nova, flavor.id)
559     if os_flavor:
560         return os_flavor.get_keys()
561
562
563 def add_security_group(nova, vm, security_group_name):
564     """
565     Adds a security group to an existing VM
566     :param nova: the nova client
567     :param vm: the OpenStack server object (VM) to alter
568     :param security_group_name: the name of the security group to add
569     """
570     nova.servers.add_security_group(str(vm.id), security_group_name)
571
572
573 def remove_security_group(nova, vm, security_group):
574     """
575     Removes a security group from an existing VM
576     :param nova: the nova client
577     :param vm: the OpenStack server object (VM) to alter
578     :param security_group: the SNAPS SecurityGroup domain object to add
579     """
580     nova.servers.remove_security_group(str(vm.id), security_group.name)
581
582
583 def add_floating_ip_to_server(nova, vm, floating_ip, ip_addr):
584     """
585     Adds a floating IP to a server instance
586     :param nova: the nova client
587     :param vm: VmInst domain object
588     :param floating_ip: FloatingIp domain object
589     :param ip_addr: the IP to which to bind the floating IP to
590     """
591     vm = __get_latest_server_os_object(nova, vm)
592     vm.add_floating_ip(floating_ip.ip, ip_addr)
593
594
595 def get_compute_quotas(nova, project_id):
596     """
597     Returns a list of all available keypairs
598     :param nova: the Nova client
599     :param project_id: the project's ID of the quotas to lookup
600     :return: an object of type ComputeQuotas or None if not found
601     """
602     quotas = nova.quotas.get(tenant_id=project_id)
603     if quotas:
604         return ComputeQuotas(quotas)
605
606
607 def update_quotas(nova, project_id, compute_quotas):
608     """
609     Updates the compute quotas for a given project
610     :param nova: the Nova client
611     :param project_id: the project's ID that requires quota updates
612     :param compute_quotas: an object of type ComputeQuotas containing the
613                            values to update
614     :return:
615     """
616     update_values = dict()
617     update_values['metadata_items'] = compute_quotas.metadata_items
618     update_values['cores'] = compute_quotas.cores
619     update_values['instances'] = compute_quotas.instances
620     update_values['injected_files'] = compute_quotas.injected_files
621     update_values['injected_file_content_bytes'] = compute_quotas.injected_file_content_bytes
622     update_values['ram'] = compute_quotas.ram
623     update_values['fixed_ips'] = compute_quotas.fixed_ips
624     update_values['key_pairs'] = compute_quotas.key_pairs
625
626     return nova.quotas.update(project_id, **update_values)
627
628
629 class NovaException(Exception):
630     """
631     Exception when calls to the Keystone client cannot be served properly
632     """