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