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