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