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