d6f9a194024c21814a662b561fcf1094132a6a4f
[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.vm_inst import VmInst
27 from snaps.openstack.utils import keystone_utils, glance_utils, neutron_utils
28
29 __author__ = 'spisarski'
30
31 logger = logging.getLogger('nova_utils')
32
33 """
34 Utilities for basic OpenStack Nova API calls
35 """
36
37
38 def nova_client(os_creds):
39     """
40     Instantiates and returns a client for communications with OpenStack's Nova
41     server
42     :param os_creds: The connection credentials to the OpenStack API
43     :return: the client object
44     """
45     logger.debug('Retrieving Nova Client')
46     return Client(os_creds.compute_api_version,
47                   session=keystone_utils.keystone_session(os_creds))
48
49
50 def create_server(nova, neutron, glance, instance_settings, image_settings,
51                   keypair_settings=None):
52     """
53     Creates a VM instance
54     :param nova: the nova client (required)
55     :param neutron: the neutron client for retrieving ports (required)
56     :param glance: the glance client (required)
57     :param instance_settings: the VM instance settings object (required)
58     :param image_settings: the VM's image settings object (required)
59     :param keypair_settings: the VM's keypair settings object (optional)
60     :return: a snaps.domain.VmInst object
61     """
62
63     ports = list()
64
65     for port_setting in instance_settings.port_settings:
66         ports.append(neutron_utils.get_port_by_name(
67             neutron, port_setting.name))
68     nics = []
69     for port in ports:
70         kv = dict()
71         kv['port-id'] = port['port']['id']
72         nics.append(kv)
73
74     logger.info('Creating VM with name - ' + instance_settings.name)
75     keypair_name = None
76     if keypair_settings:
77         keypair_name = keypair_settings.name
78
79     flavor = get_flavor_by_name(nova, instance_settings.flavor)
80     if not flavor:
81         raise Exception(
82             'Flavor not found with name - %s',
83             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 Exception(
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     return nova.keypairs.create(name=name, public_key=key.decode('utf-8'))
228
229
230 def keypair_exists(nova, keypair_obj):
231     """
232     Returns a copy of the keypair object if found
233     :param nova: the Nova client
234     :param keypair_obj: the keypair object
235     :return: the keypair object or None if not found
236     """
237     try:
238         return nova.keypairs.get(keypair_obj)
239     except:
240         return None
241
242
243 def get_keypair_by_name(nova, name):
244     """
245     Returns a list of all available keypairs
246     :param nova: the Nova client
247     :param name: the name of the keypair to lookup
248     :return: the keypair object or None if not found
249     """
250     keypairs = nova.keypairs.list()
251
252     for keypair in keypairs:
253         if keypair.name == name:
254             return keypair
255
256     return None
257
258
259 def delete_keypair(nova, key):
260     """
261     Deletes a keypair object from OpenStack
262     :param nova: the Nova client
263     :param key: the keypair object to delete
264     """
265     logger.debug('Deleting keypair - ' + key.name)
266     nova.keypairs.delete(key)
267
268
269 def get_nova_availability_zones(nova):
270     """
271     Returns the names of all nova active compute servers
272     :param nova: the Nova client
273     :return: a list of compute server names
274     """
275     out = list()
276     zones = nova.availability_zones.list()
277     for zone in zones:
278         if zone.zoneName == 'nova':
279             for key, host in zone.hosts.items():
280                 if host['nova-compute']['available']:
281                     out.append(zone.zoneName + ':' + key)
282
283     return out
284
285
286 def delete_vm_instance(nova, vm_inst):
287     """
288     Deletes a VM instance
289     :param nova: the nova client
290     :param vm_inst: the snaps.domain.VmInst object
291     """
292     nova.servers.delete(vm_inst.id)
293
294
295 def get_os_flavor(nova, flavor):
296     """
297     Returns to OpenStack flavor object by name
298     :param nova: the Nova client
299     :param flavor: the SNAPS flavor domain object
300     :return: the OpenStack Flavor object
301     """
302     try:
303         return nova.flavors.get(flavor.id)
304     except NotFound:
305         return None
306
307
308 def get_flavor(nova, flavor):
309     """
310     Returns to OpenStack flavor object by name
311     :param nova: the Nova client
312     :param flavor: the SNAPS flavor domain object
313     :return: the SNAPS Flavor domain object
314     """
315     os_flavor = get_os_flavor(nova, flavor)
316     if os_flavor:
317         return Flavor(
318             name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
319             disk=os_flavor.disk, vcpus=os_flavor.vcpus,
320             ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
321             rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
322     try:
323         return nova.flavors.get(flavor.id)
324     except NotFound:
325         return None
326
327
328 def get_os_flavor_by_name(nova, name):
329     """
330     Returns to OpenStack flavor object by name
331     :param nova: the Nova client
332     :param name: the name of the flavor to query
333     :return: OpenStack flavor object
334     """
335     try:
336         return nova.flavors.find(name=name)
337     except NotFound:
338         return None
339
340
341 def get_flavor_by_name(nova, name):
342     """
343     Returns a flavor by name
344     :param nova: the Nova client
345     :param name: the flavor name to return
346     :return: the SNAPS flavor domain object or None if not exists
347     """
348     os_flavor = get_os_flavor_by_name(nova, name)
349     if os_flavor:
350         return Flavor(
351             name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
352             disk=os_flavor.disk, vcpus=os_flavor.vcpus,
353             ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
354             rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
355
356
357 def create_flavor(nova, flavor_settings):
358     """
359     Creates and returns and OpenStack flavor object
360     :param nova: the Nova client
361     :param flavor_settings: the flavor settings
362     :return: the SNAPS flavor domain object
363     """
364     os_flavor = nova.flavors.create(
365         name=flavor_settings.name, flavorid=flavor_settings.flavor_id,
366         ram=flavor_settings.ram, vcpus=flavor_settings.vcpus,
367         disk=flavor_settings.disk, ephemeral=flavor_settings.ephemeral,
368         swap=flavor_settings.swap, rxtx_factor=flavor_settings.rxtx_factor,
369         is_public=flavor_settings.is_public)
370     return Flavor(
371         name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
372         disk=os_flavor.disk, vcpus=os_flavor.vcpus,
373         ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
374         rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
375
376
377 def delete_flavor(nova, flavor):
378     """
379     Deletes a flavor
380     :param nova: the Nova client
381     :param flavor: the SNAPS flavor domain object
382     """
383     nova.flavors.delete(flavor.id)
384
385
386 def set_flavor_keys(nova, flavor, metadata):
387     """
388     Sets metadata on the flavor
389     :param nova: the Nova client
390     :param flavor: the SNAPS flavor domain object
391     :param metadata: the metadata to set
392     """
393     os_flavor = get_os_flavor(nova, flavor)
394     if os_flavor:
395         os_flavor.set_keys(metadata)
396
397
398 def add_security_group(nova, vm, security_group_name):
399     """
400     Adds a security group to an existing VM
401     :param nova: the nova client
402     :param vm: the OpenStack server object (VM) to alter
403     :param security_group_name: the name of the security group to add
404     """
405     nova.servers.add_security_group(str(vm.id), security_group_name)
406
407
408 def remove_security_group(nova, vm, security_group):
409     """
410     Removes a security group from an existing VM
411     :param nova: the nova client
412     :param vm: the OpenStack server object (VM) to alter
413     :param security_group: the OpenStack security group object to add
414     """
415     nova.servers.remove_security_group(
416         str(vm.id), security_group['security_group']['name'])
417
418
419 def add_floating_ip_to_server(nova, vm, floating_ip, ip_addr):
420     """
421     Adds a floating IP to a server instance
422     :param nova: the nova client
423     :param vm: VmInst domain object
424     :param floating_ip: FloatingIp domain object
425     :param ip_addr: the IP to which to bind the floating IP to
426     """
427     vm = get_latest_server_os_object(nova, vm)
428     vm.add_floating_ip(floating_ip.ip, ip_addr)