Added region support.
[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             public_handle = open(pub_expand_file, 'wb')
236             public_bytes = keys.public_key().public_bytes(
237                 serialization.Encoding.OpenSSH,
238                 serialization.PublicFormat.OpenSSH)
239             public_handle.write(public_bytes)
240             public_handle.close()
241             os.chmod(pub_expand_file, 0o400)
242             logger.info("Saved public key to - " + pub_expand_file)
243         if priv_file_path:
244             # To support '~'
245             priv_expand_file = os.path.expanduser(priv_file_path)
246             priv_dir = os.path.dirname(priv_expand_file)
247             if not os.path.isdir(priv_dir):
248                 os.mkdir(priv_dir)
249             private_handle = open(priv_expand_file, 'wb')
250             private_handle.write(
251                 keys.private_bytes(
252                     encoding=serialization.Encoding.PEM,
253                     format=serialization.PrivateFormat.TraditionalOpenSSL,
254                     encryption_algorithm=serialization.NoEncryption()))
255             private_handle.close()
256             os.chmod(priv_expand_file, 0o400)
257             logger.info("Saved private key to - " + priv_expand_file)
258
259
260 def upload_keypair_file(nova, name, file_path):
261     """
262     Uploads a public key from a file
263     :param nova: the Nova client
264     :param name: the keypair name
265     :param file_path: the path to the public key file
266     :return: the keypair object
267     """
268     with open(os.path.expanduser(file_path), 'rb') as fpubkey:
269         logger.info('Saving keypair to - ' + file_path)
270         return upload_keypair(nova, name, fpubkey.read())
271
272
273 def upload_keypair(nova, name, key):
274     """
275     Uploads a public key from a file
276     :param nova: the Nova client
277     :param name: the keypair name
278     :param key: the public key object
279     :return: the keypair object
280     """
281     logger.info('Creating keypair with name - ' + name)
282     os_kp = nova.keypairs.create(name=name, public_key=key.decode('utf-8'))
283     return Keypair(name=os_kp.name, id=os_kp.id, public_key=os_kp.public_key)
284
285
286 def keypair_exists(nova, keypair_obj):
287     """
288     Returns a copy of the keypair object if found
289     :param nova: the Nova client
290     :param keypair_obj: the keypair object
291     :return: the keypair object or None if not found
292     """
293     try:
294         os_kp = nova.keypairs.get(keypair_obj)
295         return Keypair(name=os_kp.name, id=os_kp.id,
296                        public_key=os_kp.public_key)
297     except:
298         return None
299
300
301 def get_keypair_by_name(nova, name):
302     """
303     Returns a list of all available keypairs
304     :param nova: the Nova client
305     :param name: the name of the keypair to lookup
306     :return: the keypair object or None if not found
307     """
308     keypairs = nova.keypairs.list()
309
310     for keypair in keypairs:
311         if keypair.name == name:
312             return Keypair(name=keypair.name, id=keypair.id,
313                            public_key=keypair.public_key)
314
315     return None
316
317
318 def delete_keypair(nova, key):
319     """
320     Deletes a keypair object from OpenStack
321     :param nova: the Nova client
322     :param key: the SNAPS-OO keypair domain object to delete
323     """
324     logger.debug('Deleting keypair - ' + key.name)
325     nova.keypairs.delete(key.id)
326
327
328 def get_availability_zone_hosts(nova, zone_name='nova'):
329     """
330     Returns the names of all nova active compute servers
331     :param nova: the Nova client
332     :param zone_name: the Nova client
333     :return: a list of compute server names
334     """
335     out = list()
336     zones = nova.availability_zones.list()
337     for zone in zones:
338         if zone.zoneName == zone_name and zone.hosts:
339             for key, host in zone.hosts.items():
340                 if host['nova-compute']['available']:
341                     out.append(zone.zoneName + ':' + key)
342
343     return out
344
345
346 def delete_vm_instance(nova, vm_inst):
347     """
348     Deletes a VM instance
349     :param nova: the nova client
350     :param vm_inst: the snaps.domain.VmInst object
351     """
352     nova.servers.delete(vm_inst.id)
353
354
355 def __get_os_flavor(nova, flavor):
356     """
357     Returns to OpenStack flavor object by name
358     :param nova: the Nova client
359     :param flavor: the SNAPS flavor domain object
360     :return: the OpenStack Flavor object
361     """
362     try:
363         return nova.flavors.get(flavor.id)
364     except NotFound:
365         return None
366
367
368 def get_flavor(nova, flavor):
369     """
370     Returns to OpenStack flavor object by name
371     :param nova: the Nova client
372     :param flavor: the SNAPS flavor domain object
373     :return: the SNAPS Flavor domain object
374     """
375     os_flavor = __get_os_flavor(nova, flavor)
376     if os_flavor:
377         return Flavor(
378             name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
379             disk=os_flavor.disk, vcpus=os_flavor.vcpus,
380             ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
381             rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
382     try:
383         return nova.flavors.get(flavor.id)
384     except NotFound:
385         return None
386
387
388 def __get_os_flavor_by_name(nova, name):
389     """
390     Returns to OpenStack flavor object by name
391     :param nova: the Nova client
392     :param name: the name of the flavor to query
393     :return: OpenStack flavor object
394     """
395     try:
396         return nova.flavors.find(name=name)
397     except NotFound:
398         return None
399
400
401 def get_flavor_by_name(nova, name):
402     """
403     Returns a flavor by name
404     :param nova: the Nova client
405     :param name: the flavor name to return
406     :return: the SNAPS flavor domain object or None if not exists
407     """
408     os_flavor = __get_os_flavor_by_name(nova, name)
409     if os_flavor:
410         return Flavor(
411             name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
412             disk=os_flavor.disk, vcpus=os_flavor.vcpus,
413             ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
414             rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
415
416
417 def create_flavor(nova, flavor_settings):
418     """
419     Creates and returns and OpenStack flavor object
420     :param nova: the Nova client
421     :param flavor_settings: the flavor settings
422     :return: the SNAPS flavor domain object
423     """
424     os_flavor = nova.flavors.create(
425         name=flavor_settings.name, flavorid=flavor_settings.flavor_id,
426         ram=flavor_settings.ram, vcpus=flavor_settings.vcpus,
427         disk=flavor_settings.disk, ephemeral=flavor_settings.ephemeral,
428         swap=flavor_settings.swap, rxtx_factor=flavor_settings.rxtx_factor,
429         is_public=flavor_settings.is_public)
430     return Flavor(
431         name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
432         disk=os_flavor.disk, vcpus=os_flavor.vcpus,
433         ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
434         rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
435
436
437 def delete_flavor(nova, flavor):
438     """
439     Deletes a flavor
440     :param nova: the Nova client
441     :param flavor: the SNAPS flavor domain object
442     """
443     nova.flavors.delete(flavor.id)
444
445
446 def set_flavor_keys(nova, flavor, metadata):
447     """
448     Sets metadata on the flavor
449     :param nova: the Nova client
450     :param flavor: the SNAPS flavor domain object
451     :param metadata: the metadata to set
452     """
453     os_flavor = __get_os_flavor(nova, flavor)
454     if os_flavor:
455         os_flavor.set_keys(metadata)
456
457
458 def get_flavor_keys(nova, flavor):
459     """
460     Sets metadata on the flavor
461     :param nova: the Nova client
462     :param flavor: the SNAPS flavor domain object
463     """
464     os_flavor = __get_os_flavor(nova, flavor)
465     if os_flavor:
466         return os_flavor.get_keys()
467
468
469 def add_security_group(nova, vm, security_group_name):
470     """
471     Adds a security group to an existing VM
472     :param nova: the nova client
473     :param vm: the OpenStack server object (VM) to alter
474     :param security_group_name: the name of the security group to add
475     """
476     nova.servers.add_security_group(str(vm.id), security_group_name)
477
478
479 def remove_security_group(nova, vm, security_group):
480     """
481     Removes a security group from an existing VM
482     :param nova: the nova client
483     :param vm: the OpenStack server object (VM) to alter
484     :param security_group: the SNAPS SecurityGroup domain object to add
485     """
486     nova.servers.remove_security_group(str(vm.id), security_group.name)
487
488
489 def add_floating_ip_to_server(nova, vm, floating_ip, ip_addr):
490     """
491     Adds a floating IP to a server instance
492     :param nova: the nova client
493     :param vm: VmInst domain object
494     :param floating_ip: FloatingIp domain object
495     :param ip_addr: the IP to which to bind the floating IP to
496     """
497     vm = __get_latest_server_os_object(nova, vm)
498     vm.add_floating_ip(floating_ip.ip, ip_addr)
499
500
501 class NovaException(Exception):
502     """
503     Exception when calls to the Keystone client cannot be served properly
504     """