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