Created new class NovaException.
[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 servers
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_latest_server_object(nova, server):
132     """
133     Returns a server with a given id
134     :param nova: the Nova client
135     :param server: the old server object
136     :return: the list of servers or None if not found
137     """
138     server = get_latest_server_os_object(nova, server)
139     return VmInst(name=server.name, inst_id=server.id,
140                   networks=server.networks)
141
142
143 def create_keys(key_size=2048):
144     """
145     Generates public and private keys
146     :param key_size: the number of bytes for the key size
147     :return: the cryptography keys
148     """
149     return rsa.generate_private_key(backend=default_backend(),
150                                     public_exponent=65537,
151                                     key_size=key_size)
152
153
154 def public_key_openssh(keys):
155     """
156     Returns the public key for OpenSSH
157     :param keys: the keys generated by create_keys() from cryptography
158     :return: the OpenSSH public key
159     """
160     return keys.public_key().public_bytes(serialization.Encoding.OpenSSH,
161                                           serialization.PublicFormat.OpenSSH)
162
163
164 def save_keys_to_files(keys=None, pub_file_path=None, priv_file_path=None):
165     """
166     Saves the generated RSA generated keys to the filesystem
167     :param keys: the keys to save generated by cryptography
168     :param pub_file_path: the path to the public keys
169     :param priv_file_path: the path to the private keys
170     :return: None
171     """
172     if keys:
173         if pub_file_path:
174             # To support '~'
175             pub_expand_file = os.path.expanduser(pub_file_path)
176             pub_dir = os.path.dirname(pub_expand_file)
177
178             if not os.path.isdir(pub_dir):
179                 os.mkdir(pub_dir)
180             public_handle = open(pub_expand_file, 'wb')
181             public_bytes = keys.public_key().public_bytes(
182                 serialization.Encoding.OpenSSH,
183                 serialization.PublicFormat.OpenSSH)
184             public_handle.write(public_bytes)
185             public_handle.close()
186             os.chmod(pub_expand_file, 0o400)
187             logger.info("Saved public key to - " + pub_expand_file)
188         if priv_file_path:
189             # To support '~'
190             priv_expand_file = os.path.expanduser(priv_file_path)
191             priv_dir = os.path.dirname(priv_expand_file)
192             if not os.path.isdir(priv_dir):
193                 os.mkdir(priv_dir)
194             private_handle = open(priv_expand_file, 'wb')
195             private_handle.write(
196                 keys.private_bytes(
197                     encoding=serialization.Encoding.PEM,
198                     format=serialization.PrivateFormat.TraditionalOpenSSL,
199                     encryption_algorithm=serialization.NoEncryption()))
200             private_handle.close()
201             os.chmod(priv_expand_file, 0o400)
202             logger.info("Saved private key to - " + priv_expand_file)
203
204
205 def upload_keypair_file(nova, name, file_path):
206     """
207     Uploads a public key from a file
208     :param nova: the Nova client
209     :param name: the keypair name
210     :param file_path: the path to the public key file
211     :return: the keypair object
212     """
213     with open(os.path.expanduser(file_path), 'rb') as fpubkey:
214         logger.info('Saving keypair to - ' + file_path)
215         return upload_keypair(nova, name, fpubkey.read())
216
217
218 def upload_keypair(nova, name, key):
219     """
220     Uploads a public key from a file
221     :param nova: the Nova client
222     :param name: the keypair name
223     :param key: the public key object
224     :return: the keypair object
225     """
226     logger.info('Creating keypair with name - ' + name)
227     os_kp = nova.keypairs.create(name=name, public_key=key.decode('utf-8'))
228     return Keypair(name=os_kp.name, id=os_kp.id, public_key=os_kp.public_key)
229
230
231 def keypair_exists(nova, keypair_obj):
232     """
233     Returns a copy of the keypair object if found
234     :param nova: the Nova client
235     :param keypair_obj: the keypair object
236     :return: the keypair object or None if not found
237     """
238     try:
239         os_kp = nova.keypairs.get(keypair_obj)
240         return Keypair(name=os_kp.name, id=os_kp.id,
241                        public_key=os_kp.public_key)
242     except:
243         return None
244
245
246 def get_keypair_by_name(nova, name):
247     """
248     Returns a list of all available keypairs
249     :param nova: the Nova client
250     :param name: the name of the keypair to lookup
251     :return: the keypair object or None if not found
252     """
253     keypairs = nova.keypairs.list()
254
255     for keypair in keypairs:
256         if keypair.name == name:
257             return Keypair(name=keypair.name, id=keypair.id,
258                            public_key=keypair.public_key)
259
260     return None
261
262
263 def delete_keypair(nova, key):
264     """
265     Deletes a keypair object from OpenStack
266     :param nova: the Nova client
267     :param key: the SNAPS-OO keypair domain object to delete
268     """
269     logger.debug('Deleting keypair - ' + key.name)
270     nova.keypairs.delete(key.id)
271
272
273 def get_nova_availability_zones(nova):
274     """
275     Returns the names of all nova active compute servers
276     :param nova: the Nova client
277     :return: a list of compute server names
278     """
279     out = list()
280     zones = nova.availability_zones.list()
281     for zone in zones:
282         if zone.zoneName == 'nova':
283             for key, host in zone.hosts.items():
284                 if host['nova-compute']['available']:
285                     out.append(zone.zoneName + ':' + key)
286
287     return out
288
289
290 def delete_vm_instance(nova, vm_inst):
291     """
292     Deletes a VM instance
293     :param nova: the nova client
294     :param vm_inst: the snaps.domain.VmInst object
295     """
296     nova.servers.delete(vm_inst.id)
297
298
299 def get_os_flavor(nova, flavor):
300     """
301     Returns to OpenStack flavor object by name
302     :param nova: the Nova client
303     :param flavor: the SNAPS flavor domain object
304     :return: the OpenStack Flavor object
305     """
306     try:
307         return nova.flavors.get(flavor.id)
308     except NotFound:
309         return None
310
311
312 def get_flavor(nova, flavor):
313     """
314     Returns to OpenStack flavor object by name
315     :param nova: the Nova client
316     :param flavor: the SNAPS flavor domain object
317     :return: the SNAPS Flavor domain object
318     """
319     os_flavor = get_os_flavor(nova, flavor)
320     if os_flavor:
321         return Flavor(
322             name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
323             disk=os_flavor.disk, vcpus=os_flavor.vcpus,
324             ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
325             rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
326     try:
327         return nova.flavors.get(flavor.id)
328     except NotFound:
329         return None
330
331
332 def get_os_flavor_by_name(nova, name):
333     """
334     Returns to OpenStack flavor object by name
335     :param nova: the Nova client
336     :param name: the name of the flavor to query
337     :return: OpenStack flavor object
338     """
339     try:
340         return nova.flavors.find(name=name)
341     except NotFound:
342         return None
343
344
345 def get_flavor_by_name(nova, name):
346     """
347     Returns a flavor by name
348     :param nova: the Nova client
349     :param name: the flavor name to return
350     :return: the SNAPS flavor domain object or None if not exists
351     """
352     os_flavor = get_os_flavor_by_name(nova, name)
353     if os_flavor:
354         return Flavor(
355             name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
356             disk=os_flavor.disk, vcpus=os_flavor.vcpus,
357             ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
358             rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
359
360
361 def create_flavor(nova, flavor_settings):
362     """
363     Creates and returns and OpenStack flavor object
364     :param nova: the Nova client
365     :param flavor_settings: the flavor settings
366     :return: the SNAPS flavor domain object
367     """
368     os_flavor = nova.flavors.create(
369         name=flavor_settings.name, flavorid=flavor_settings.flavor_id,
370         ram=flavor_settings.ram, vcpus=flavor_settings.vcpus,
371         disk=flavor_settings.disk, ephemeral=flavor_settings.ephemeral,
372         swap=flavor_settings.swap, rxtx_factor=flavor_settings.rxtx_factor,
373         is_public=flavor_settings.is_public)
374     return Flavor(
375         name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
376         disk=os_flavor.disk, vcpus=os_flavor.vcpus,
377         ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
378         rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
379
380
381 def delete_flavor(nova, flavor):
382     """
383     Deletes a flavor
384     :param nova: the Nova client
385     :param flavor: the SNAPS flavor domain object
386     """
387     nova.flavors.delete(flavor.id)
388
389
390 def set_flavor_keys(nova, flavor, metadata):
391     """
392     Sets metadata on the flavor
393     :param nova: the Nova client
394     :param flavor: the SNAPS flavor domain object
395     :param metadata: the metadata to set
396     """
397     os_flavor = get_os_flavor(nova, flavor)
398     if os_flavor:
399         os_flavor.set_keys(metadata)
400
401
402 def add_security_group(nova, vm, security_group_name):
403     """
404     Adds a security group to an existing VM
405     :param nova: the nova client
406     :param vm: the OpenStack server object (VM) to alter
407     :param security_group_name: the name of the security group to add
408     """
409     nova.servers.add_security_group(str(vm.id), security_group_name)
410
411
412 def remove_security_group(nova, vm, security_group):
413     """
414     Removes a security group from an existing VM
415     :param nova: the nova client
416     :param vm: the OpenStack server object (VM) to alter
417     :param security_group: the SNAPS SecurityGroup domain object to add
418     """
419     nova.servers.remove_security_group(str(vm.id), security_group.name)
420
421
422 def add_floating_ip_to_server(nova, vm, floating_ip, ip_addr):
423     """
424     Adds a floating IP to a server instance
425     :param nova: the nova client
426     :param vm: VmInst domain object
427     :param floating_ip: FloatingIp domain object
428     :param ip_addr: the IP to which to bind the floating IP to
429     """
430     vm = get_latest_server_os_object(nova, vm)
431     vm.add_floating_ip(floating_ip.ip, ip_addr)
432
433
434 class NovaException(Exception):
435     """
436     Exception when calls to the Keystone client cannot be served properly
437     """