Ensure library and tests close all necessary resources.
[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.domain.flavor import Flavor
26 from snaps.domain.keypair import Keypair
27 from snaps.domain.vm_inst import VmInst
28 from snaps.openstack.utils import keystone_utils, glance_utils, neutron_utils
29
30 __author__ = 'spisarski'
31
32 logger = logging.getLogger('nova_utils')
33
34 """
35 Utilities for basic OpenStack Nova API calls
36 """
37
38
39 def nova_client(os_creds):
40     """
41     Instantiates and returns a client for communications with OpenStack's Nova
42     server
43     :param os_creds: The connection credentials to the OpenStack API
44     :return: the client object
45     """
46     logger.debug('Retrieving Nova Client')
47     return Client(os_creds.compute_api_version,
48                   session=keystone_utils.keystone_session(os_creds),
49                   region_name=os_creds.region_name)
50
51
52 def create_server(nova, neutron, glance, instance_settings, image_settings,
53                   keypair_settings=None):
54     """
55     Creates a VM instance
56     :param nova: the nova client (required)
57     :param neutron: the neutron client for retrieving ports (required)
58     :param glance: the glance client (required)
59     :param instance_settings: the VM instance settings object (required)
60     :param image_settings: the VM's image settings object (required)
61     :param keypair_settings: the VM's keypair settings object (optional)
62     :return: a snaps.domain.VmInst object
63     """
64
65     ports = list()
66
67     for port_setting in instance_settings.port_settings:
68         ports.append(neutron_utils.get_port_by_name(
69             neutron, port_setting.name))
70     nics = []
71     for port in ports:
72         kv = dict()
73         kv['port-id'] = port.id
74         nics.append(kv)
75
76     logger.info('Creating VM with name - ' + instance_settings.name)
77     keypair_name = None
78     if keypair_settings:
79         keypair_name = keypair_settings.name
80
81     flavor = get_flavor_by_name(nova, instance_settings.flavor)
82     if not flavor:
83         raise NovaException(
84             'Flavor not found with name - %s', instance_settings.flavor)
85
86     image = glance_utils.get_image(glance, image_settings.name)
87     if image:
88         args = {'name': instance_settings.name,
89                 'flavor': flavor,
90                 'image': image,
91                 'nics': nics,
92                 'key_name': keypair_name,
93                 'security_groups':
94                     instance_settings.security_group_names,
95                 'userdata': instance_settings.userdata}
96
97         if instance_settings.availability_zone:
98             args['availability_zone'] = instance_settings.availability_zone
99
100         server = nova.servers.create(**args)
101         return VmInst(name=server.name, inst_id=server.id,
102                       networks=server.networks)
103     else:
104         raise NovaException(
105             'Cannot create instance, image cannot be located with name %s',
106             image_settings.name)
107
108
109 def get_servers_by_name(nova, name):
110     """
111     Returns a list of servers with a given name
112     :param nova: the Nova client
113     :param name: the server name
114     :return: the list of snaps.domain.VmInst objects
115     """
116     out = list()
117     servers = nova.servers.list(search_opts={'name': name})
118     for server in servers:
119         out.append(VmInst(name=server.name, inst_id=server.id,
120                           networks=server.networks))
121     return out
122
123
124 def __get_latest_server_os_object(nova, server):
125     """
126     Returns a server with a given id
127     :param nova: the Nova client
128     :param server: the domain VmInst object
129     :return: the list of servers or None if not found
130     """
131     return nova.servers.get(server.id)
132
133
134 def get_server_status(nova, server):
135     """
136     Returns the a VM instance's status from OpenStack
137     :param nova: the Nova client
138     :param server: the domain VmInst object
139     :return: the VM's string status or None if not founc
140     """
141     server = __get_latest_server_os_object(nova, server)
142     if server:
143         return server.status
144     return None
145
146
147 def get_server_console_output(nova, server):
148     """
149     Returns the console object for parsing VM activity
150     :param nova: the Nova client
151     :param server: the domain VmInst object
152     :return: the console output object or None if server object is not found
153     """
154     server = __get_latest_server_os_object(nova, server)
155     if server:
156         return server.get_console_output()
157     return None
158
159
160 def get_latest_server_object(nova, server):
161     """
162     Returns a server with a given id
163     :param nova: the Nova client
164     :param server: the old server object
165     :return: the list of servers or None if not found
166     """
167     server = __get_latest_server_os_object(nova, server)
168     return VmInst(name=server.name, inst_id=server.id,
169                   networks=server.networks)
170
171
172 def get_server_security_group_names(nova, server):
173     """
174     Returns a server with a given id
175     :param nova: the Nova client
176     :param server: the old server object
177     :return: the list of security groups associated with a VM
178     """
179     out = list()
180     os_vm_inst = __get_latest_server_os_object(nova, server)
181     for sec_grp_dict in os_vm_inst.security_groups:
182         out.append(sec_grp_dict['name'])
183     return out
184
185
186 def get_server_info(nova, server):
187     """
188     Returns a dictionary of a VMs info as returned by OpenStack
189     :param nova: the Nova client
190     :param server: the old server object
191     :return: a dict of the info if VM exists else None
192     """
193     vm = __get_latest_server_os_object(nova, server)
194     if vm:
195         return vm._info
196     return None
197
198
199 def create_keys(key_size=2048):
200     """
201     Generates public and private keys
202     :param key_size: the number of bytes for the key size
203     :return: the cryptography keys
204     """
205     return rsa.generate_private_key(backend=default_backend(),
206                                     public_exponent=65537,
207                                     key_size=key_size)
208
209
210 def public_key_openssh(keys):
211     """
212     Returns the public key for OpenSSH
213     :param keys: the keys generated by create_keys() from cryptography
214     :return: the OpenSSH public key
215     """
216     return keys.public_key().public_bytes(serialization.Encoding.OpenSSH,
217                                           serialization.PublicFormat.OpenSSH)
218
219
220 def save_keys_to_files(keys=None, pub_file_path=None, priv_file_path=None):
221     """
222     Saves the generated RSA generated keys to the filesystem
223     :param keys: the keys to save generated by cryptography
224     :param pub_file_path: the path to the public keys
225     :param priv_file_path: the path to the private keys
226     """
227     if keys:
228         if pub_file_path:
229             # To support '~'
230             pub_expand_file = os.path.expanduser(pub_file_path)
231             pub_dir = os.path.dirname(pub_expand_file)
232
233             if not os.path.isdir(pub_dir):
234                 os.mkdir(pub_dir)
235
236             public_handle = None
237             try:
238                 public_handle = open(pub_expand_file, 'wb')
239                 public_bytes = keys.public_key().public_bytes(
240                     serialization.Encoding.OpenSSH,
241                     serialization.PublicFormat.OpenSSH)
242                 public_handle.write(public_bytes)
243             finally:
244                 if public_handle:
245                     public_handle.close()
246
247             os.chmod(pub_expand_file, 0o400)
248             logger.info("Saved public key to - " + pub_expand_file)
249         if priv_file_path:
250             # To support '~'
251             priv_expand_file = os.path.expanduser(priv_file_path)
252             priv_dir = os.path.dirname(priv_expand_file)
253             if not os.path.isdir(priv_dir):
254                 os.mkdir(priv_dir)
255
256             private_handle = None
257             try:
258                 private_handle = open(priv_expand_file, 'wb')
259                 private_handle.write(
260                     keys.private_bytes(
261                         encoding=serialization.Encoding.PEM,
262                         format=serialization.PrivateFormat.TraditionalOpenSSL,
263                         encryption_algorithm=serialization.NoEncryption()))
264             finally:
265                 if private_handle:
266                     private_handle.close()
267
268             os.chmod(priv_expand_file, 0o400)
269             logger.info("Saved private key to - " + priv_expand_file)
270
271
272 def upload_keypair_file(nova, name, file_path):
273     """
274     Uploads a public key from a file
275     :param nova: the Nova client
276     :param name: the keypair name
277     :param file_path: the path to the public key file
278     :return: the keypair object
279     """
280     fpubkey = None
281     try:
282         with open(os.path.expanduser(file_path), 'rb') as fpubkey:
283             logger.info('Saving keypair to - ' + file_path)
284             return upload_keypair(nova, name, fpubkey.read())
285     finally:
286         if fpubkey:
287             fpubkey.close()
288
289
290 def upload_keypair(nova, name, key):
291     """
292     Uploads a public key from a file
293     :param nova: the Nova client
294     :param name: the keypair name
295     :param key: the public key object
296     :return: the keypair object
297     """
298     logger.info('Creating keypair with name - ' + name)
299     os_kp = nova.keypairs.create(name=name, public_key=key.decode('utf-8'))
300     return Keypair(name=os_kp.name, id=os_kp.id, public_key=os_kp.public_key)
301
302
303 def keypair_exists(nova, keypair_obj):
304     """
305     Returns a copy of the keypair object if found
306     :param nova: the Nova client
307     :param keypair_obj: the keypair object
308     :return: the keypair object or None if not found
309     """
310     try:
311         os_kp = nova.keypairs.get(keypair_obj)
312         return Keypair(name=os_kp.name, id=os_kp.id,
313                        public_key=os_kp.public_key)
314     except:
315         return None
316
317
318 def get_keypair_by_name(nova, name):
319     """
320     Returns a list of all available keypairs
321     :param nova: the Nova client
322     :param name: the name of the keypair to lookup
323     :return: the keypair object or None if not found
324     """
325     keypairs = nova.keypairs.list()
326
327     for keypair in keypairs:
328         if keypair.name == name:
329             return Keypair(name=keypair.name, id=keypair.id,
330                            public_key=keypair.public_key)
331
332     return None
333
334
335 def delete_keypair(nova, key):
336     """
337     Deletes a keypair object from OpenStack
338     :param nova: the Nova client
339     :param key: the SNAPS-OO keypair domain object to delete
340     """
341     logger.debug('Deleting keypair - ' + key.name)
342     nova.keypairs.delete(key.id)
343
344
345 def get_availability_zone_hosts(nova, zone_name='nova'):
346     """
347     Returns the names of all nova active compute servers
348     :param nova: the Nova client
349     :param zone_name: the Nova client
350     :return: a list of compute server names
351     """
352     out = list()
353     zones = nova.availability_zones.list()
354     for zone in zones:
355         if zone.zoneName == zone_name and zone.hosts:
356             for key, host in zone.hosts.items():
357                 if host['nova-compute']['available']:
358                     out.append(zone.zoneName + ':' + key)
359
360     return out
361
362
363 def delete_vm_instance(nova, vm_inst):
364     """
365     Deletes a VM instance
366     :param nova: the nova client
367     :param vm_inst: the snaps.domain.VmInst object
368     """
369     nova.servers.delete(vm_inst.id)
370
371
372 def __get_os_flavor(nova, flavor):
373     """
374     Returns to OpenStack flavor object by name
375     :param nova: the Nova client
376     :param flavor: the SNAPS flavor domain object
377     :return: the OpenStack Flavor object
378     """
379     try:
380         return nova.flavors.get(flavor.id)
381     except NotFound:
382         return None
383
384
385 def get_flavor(nova, flavor):
386     """
387     Returns to OpenStack flavor object by name
388     :param nova: the Nova client
389     :param flavor: the SNAPS flavor domain object
390     :return: the SNAPS Flavor domain object
391     """
392     os_flavor = __get_os_flavor(nova, flavor)
393     if os_flavor:
394         return Flavor(
395             name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
396             disk=os_flavor.disk, vcpus=os_flavor.vcpus,
397             ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
398             rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
399     try:
400         return nova.flavors.get(flavor.id)
401     except NotFound:
402         return None
403
404
405 def __get_os_flavor_by_name(nova, name):
406     """
407     Returns to OpenStack flavor object by name
408     :param nova: the Nova client
409     :param name: the name of the flavor to query
410     :return: OpenStack flavor object
411     """
412     try:
413         return nova.flavors.find(name=name)
414     except NotFound:
415         return None
416
417
418 def get_flavor_by_name(nova, name):
419     """
420     Returns a flavor by name
421     :param nova: the Nova client
422     :param name: the flavor name to return
423     :return: the SNAPS flavor domain object or None if not exists
424     """
425     os_flavor = __get_os_flavor_by_name(nova, name)
426     if os_flavor:
427         return Flavor(
428             name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
429             disk=os_flavor.disk, vcpus=os_flavor.vcpus,
430             ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
431             rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
432
433
434 def create_flavor(nova, flavor_settings):
435     """
436     Creates and returns and OpenStack flavor object
437     :param nova: the Nova client
438     :param flavor_settings: the flavor settings
439     :return: the SNAPS flavor domain object
440     """
441     os_flavor = nova.flavors.create(
442         name=flavor_settings.name, flavorid=flavor_settings.flavor_id,
443         ram=flavor_settings.ram, vcpus=flavor_settings.vcpus,
444         disk=flavor_settings.disk, ephemeral=flavor_settings.ephemeral,
445         swap=flavor_settings.swap, rxtx_factor=flavor_settings.rxtx_factor,
446         is_public=flavor_settings.is_public)
447     return Flavor(
448         name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
449         disk=os_flavor.disk, vcpus=os_flavor.vcpus,
450         ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
451         rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
452
453
454 def delete_flavor(nova, flavor):
455     """
456     Deletes a flavor
457     :param nova: the Nova client
458     :param flavor: the SNAPS flavor domain object
459     """
460     nova.flavors.delete(flavor.id)
461
462
463 def set_flavor_keys(nova, flavor, metadata):
464     """
465     Sets metadata on the flavor
466     :param nova: the Nova client
467     :param flavor: the SNAPS flavor domain object
468     :param metadata: the metadata to set
469     """
470     os_flavor = __get_os_flavor(nova, flavor)
471     if os_flavor:
472         os_flavor.set_keys(metadata)
473
474
475 def get_flavor_keys(nova, flavor):
476     """
477     Sets metadata on the flavor
478     :param nova: the Nova client
479     :param flavor: the SNAPS flavor domain object
480     """
481     os_flavor = __get_os_flavor(nova, flavor)
482     if os_flavor:
483         return os_flavor.get_keys()
484
485
486 def add_security_group(nova, vm, security_group_name):
487     """
488     Adds a security group to an existing VM
489     :param nova: the nova client
490     :param vm: the OpenStack server object (VM) to alter
491     :param security_group_name: the name of the security group to add
492     """
493     nova.servers.add_security_group(str(vm.id), security_group_name)
494
495
496 def remove_security_group(nova, vm, security_group):
497     """
498     Removes a security group from an existing VM
499     :param nova: the nova client
500     :param vm: the OpenStack server object (VM) to alter
501     :param security_group: the SNAPS SecurityGroup domain object to add
502     """
503     nova.servers.remove_security_group(str(vm.id), security_group.name)
504
505
506 def add_floating_ip_to_server(nova, vm, floating_ip, ip_addr):
507     """
508     Adds a floating IP to a server instance
509     :param nova: the nova client
510     :param vm: VmInst domain object
511     :param floating_ip: FloatingIp domain object
512     :param ip_addr: the IP to which to bind the floating IP to
513     """
514     vm = __get_latest_server_os_object(nova, vm)
515     vm.add_floating_ip(floating_ip.ip, ip_addr)
516
517
518 class NovaException(Exception):
519     """
520     Exception when calls to the Keystone client cannot be served properly
521     """