diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:03:42 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:03:42 +0000 |
commit | 66cec45960ce1d9c794e9399de15c138acb18aed (patch) | |
tree | 59cd19d69e9d56b7989b080da7c20ef1a3fe2a5a /ansible_collections/openstack/cloud/plugins/modules/server.py | |
parent | Initial commit. (diff) | |
download | ansible-66cec45960ce1d9c794e9399de15c138acb18aed.tar.xz ansible-66cec45960ce1d9c794e9399de15c138acb18aed.zip |
Adding upstream version 7.3.0+dfsg.upstream/7.3.0+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/openstack/cloud/plugins/modules/server.py')
-rw-r--r-- | ansible_collections/openstack/cloud/plugins/modules/server.py | 805 |
1 files changed, 805 insertions, 0 deletions
diff --git a/ansible_collections/openstack/cloud/plugins/modules/server.py b/ansible_collections/openstack/cloud/plugins/modules/server.py new file mode 100644 index 00000000..a3ca7d05 --- /dev/null +++ b/ansible_collections/openstack/cloud/plugins/modules/server.py @@ -0,0 +1,805 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright 2019 Red Hat, Inc. +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2013, Benno Joy <benno@ansible.com> +# Copyright (c) 2013, John Dewey <john@dewey.ws> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: server +short_description: Create/Delete Compute Instances from OpenStack +author: OpenStack Ansible SIG +description: + - Create or Remove compute instances from OpenStack. +options: + name: + description: + - Name that has to be given to the instance. It is also possible to + specify the ID of the instance instead of its name if I(state) is I(absent). + required: true + type: str + image: + description: + - The name or id of the base image to boot. + - Required when I(boot_from_volume=true) + type: str + image_exclude: + description: + - Text to use to filter image names, for the case, such as HP, where + there are multiple image names matching the common identifying + portions. image_exclude is a negative match filter - it is text that + may not exist in the image name. + type: str + default: "(deprecated)" + flavor: + description: + - The name or id of the flavor in which the new instance has to be + created. + - Exactly one of I(flavor) and I(flavor_ram) must be defined when + I(state=present). + type: str + flavor_ram: + description: + - The minimum amount of ram in MB that the flavor in which the new + instance has to be created must have. + - Exactly one of I(flavor) and I(flavor_ram) must be defined when + I(state=present). + type: int + flavor_include: + description: + - Text to use to filter flavor names, for the case, such as Rackspace, + where there are multiple flavors that have the same ram count. + flavor_include is a positive match filter - it must exist in the + flavor name. + type: str + key_name: + description: + - The key pair name to be used when creating a instance + type: str + security_groups: + description: + - Names of the security groups to which the instance should be + added. This may be a YAML list or a comma separated string. + type: list + default: ['default'] + elements: str + network: + description: + - Name or ID of a network to attach this instance to. A simpler + version of the nics parameter, only one of network or nics should + be supplied. + type: str + nics: + description: + - A list of networks to which the instance's interface should + be attached. Networks may be referenced by net-id/net-name/port-id + or port-name. + - 'Also this accepts a string containing a list of (net/port)-(id/name) + Eg: nics: "net-id=uuid-1,port-name=myport" + Only one of network or nics should be supplied.' + type: list + elements: raw + suboptions: + tag: + description: + - 'A "tag" for the specific port to be passed via metadata. + Eg: tag: test_tag' + auto_ip: + description: + - Ensure instance has public ip however the cloud wants to do that + type: bool + default: 'yes' + aliases: ['auto_floating_ip', 'public_ip'] + floating_ips: + description: + - list of valid floating IPs that pre-exist to assign to this node + type: list + elements: str + floating_ip_pools: + description: + - Name of floating IP pool from which to choose a floating IP + type: list + elements: str + meta: + description: + - 'A list of key value pairs that should be provided as a metadata to + the new instance or a string containing a list of key-value pairs. + Eg: meta: "key1=value1,key2=value2"' + type: raw + wait: + description: + - If the module should wait for the instance to be created. + type: bool + default: 'yes' + timeout: + description: + - The amount of time the module should wait for the instance to get + into active state. + default: 180 + type: int + config_drive: + description: + - Whether to boot the server with config drive enabled + type: bool + default: 'no' + userdata: + description: + - Opaque blob of data which is made available to the instance + type: str + aliases: ['user_data'] + boot_from_volume: + description: + - Should the instance boot from a persistent volume created based on + the image given. Mutually exclusive with boot_volume. + type: bool + default: 'no' + volume_size: + description: + - The size of the volume to create in GB if booting from volume based + on an image. + type: int + boot_volume: + description: + - Volume name or id to use as the volume to boot from. Implies + boot_from_volume. Mutually exclusive with image and boot_from_volume. + aliases: ['root_volume'] + type: str + terminate_volume: + description: + - If C(yes), delete volume when deleting instance (if booted from volume) + type: bool + default: 'no' + volumes: + description: + - A list of preexisting volumes names or ids to attach to the instance + default: [] + type: list + elements: str + scheduler_hints: + description: + - Arbitrary key/value pairs to the scheduler for custom use + type: dict + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + delete_fip: + description: + - When I(state) is absent and this option is true, any floating IP + associated with the instance will be deleted along with the instance. + type: bool + default: 'no' + reuse_ips: + description: + - When I(auto_ip) is true and this option is true, the I(auto_ip) code + will attempt to re-use unassigned floating ips in the project before + creating a new one. It is important to note that it is impossible + to safely do this concurrently, so if your use case involves + concurrent server creation, it is highly recommended to set this to + false and to delete the floating ip associated with a server when + the server is deleted using I(delete_fip). + type: bool + default: 'yes' + availability_zone: + description: + - Availability zone in which to create the server. + type: str + description: + description: + - Description of the server. + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Create a new instance and attaches to a network and passes metadata to the instance + openstack.cloud.server: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + timeout: 200 + flavor: 4 + nics: + - net-id: 34605f38-e52a-25d2-b6ec-754a13ffb723 + - net-name: another_network + meta: + hostname: test1 + group: uge_master + +# Create a new instance in HP Cloud AE1 region availability zone az2 and +# automatically assigns a floating IP +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + state: present + auth: + auth_url: https://identity.example.com + username: username + password: Equality7-2521 + project_name: username-project1 + name: vm1 + region_name: region-b.geo-1 + availability_zone: az2 + image: 9302692b-b787-4b52-a3a6-daebb79cb498 + key_name: test + timeout: 200 + flavor: 101 + security_groups: default + auto_ip: yes + +# Create a new instance in named cloud mordred availability zone az2 +# and assigns a pre-known floating IP +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + state: present + cloud: mordred + name: vm1 + availability_zone: az2 + image: 9302692b-b787-4b52-a3a6-daebb79cb498 + key_name: test + timeout: 200 + flavor: 101 + floating_ips: + - 12.34.56.79 + +# Create a new instance with 4G of RAM on Ubuntu Trusty, ignoring +# deprecated images +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + state: present + cloud: mordred + region_name: region-b.geo-1 + image: Ubuntu Server 14.04 + image_exclude: deprecated + flavor_ram: 4096 + +# Create a new instance with 4G of RAM on Ubuntu Trusty on a Performance node +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + cloud: rax-dfw + state: present + image: Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM) + flavor_ram: 4096 + flavor_include: Performance + +# Creates a new instance and attaches to multiple network +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance with a string + openstack.cloud.server: + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + timeout: 200 + flavor: 4 + nics: "net-id=4cb08b20-62fe-11e5-9d70-feff819cdc9f,net-id=542f0430-62fe-11e5-9d70-feff819cdc9f..." + +- name: Creates a new instance and attaches to a network and passes metadata to the instance + openstack.cloud.server: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + timeout: 200 + flavor: 4 + nics: + - net-id: 34605f38-e52a-25d2-b6ec-754a13ffb723 + - net-name: another_network + meta: "hostname=test1,group=uge_master" + +- name: Creates a new instance and attaches to a specific network + openstack.cloud.server: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + timeout: 200 + flavor: 4 + network: another_network + +# Create a new instance with 4G of RAM on a 75G Ubuntu Trusty volume +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + state: present + cloud: mordred + region_name: ams01 + image: Ubuntu Server 14.04 + flavor_ram: 4096 + boot_from_volume: True + volume_size: 75 + +# Creates a new instance with 2 volumes attached +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + state: present + cloud: mordred + region_name: ams01 + image: Ubuntu Server 14.04 + flavor_ram: 4096 + volumes: + - photos + - music + +# Creates a new instance with provisioning userdata using Cloud-Init +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + state: present + image: "Ubuntu Server 14.04" + flavor: "P-1" + network: "Production" + userdata: | + #cloud-config + chpasswd: + list: | + ubuntu:{{ default_password }} + expire: False + packages: + - ansible + package_upgrade: true + +# Creates a new instance with provisioning userdata using Bash Scripts +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + name: vm1 + state: present + image: "Ubuntu Server 14.04" + flavor: "P-1" + network: "Production" + userdata: | + {%- raw -%}#!/bin/bash + echo " up ip route add 10.0.0.0/8 via {% endraw -%}{{ intra_router }}{%- raw -%}" >> /etc/network/interfaces.d/eth0.conf + echo " down ip route del 10.0.0.0/8" >> /etc/network/interfaces.d/eth0.conf + ifdown eth0 && ifup eth0 + {% endraw %} + +# Create a new instance with server group for (anti-)affinity +# server group ID is returned from openstack.cloud.server_group module. +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + openstack.cloud.server: + state: present + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + flavor: 4 + scheduler_hints: + group: f5c8c61a-9230-400a-8ed2-3b023c190a7f + +# Create an instance with "tags" for the nic +- name: Create instance with nics "tags" + openstack.cloud.server: + state: present + auth: + auth_url: https://identity.example.com + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + flavor: 4 + nics: + - port-name: net1_port1 + tag: test_tag + - net-name: another_network + +# Deletes an instance via its ID +- name: remove an instance + hosts: localhost + tasks: + - name: remove an instance + openstack.cloud.server: + name: abcdef01-2345-6789-0abc-def0123456789 + state: absent + +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + openstack_find_nova_addresses, OpenStackModule) + + +def _parse_nics(nics): + for net in nics: + if isinstance(net, str): + for nic in net.split(','): + yield dict((nic.split('='),)) + else: + yield net + + +def _parse_meta(meta): + if isinstance(meta, str): + metas = {} + for kv_str in meta.split(","): + k, v = kv_str.split("=") + metas[k] = v + return metas + if not meta: + return {} + return meta + + +class ServerModule(OpenStackModule): + deprecated_names = ('os_server', 'openstack.cloud.os_server') + + argument_spec = dict( + name=dict(required=True), + image=dict(default=None), + image_exclude=dict(default='(deprecated)'), + flavor=dict(default=None), + flavor_ram=dict(default=None, type='int'), + flavor_include=dict(default=None), + key_name=dict(default=None), + security_groups=dict(default=['default'], type='list', elements='str'), + network=dict(default=None), + nics=dict(default=[], type='list', elements='raw'), + meta=dict(default=None, type='raw'), + userdata=dict(default=None, aliases=['user_data']), + config_drive=dict(default=False, type='bool'), + auto_ip=dict(default=True, type='bool', aliases=['auto_floating_ip', 'public_ip']), + floating_ips=dict(default=None, type='list', elements='str'), + floating_ip_pools=dict(default=None, type='list', elements='str'), + volume_size=dict(default=None, type='int'), + boot_from_volume=dict(default=False, type='bool'), + boot_volume=dict(default=None, aliases=['root_volume']), + terminate_volume=dict(default=False, type='bool'), + volumes=dict(default=[], type='list', elements='str'), + scheduler_hints=dict(default=None, type='dict'), + state=dict(default='present', choices=['absent', 'present']), + delete_fip=dict(default=False, type='bool'), + reuse_ips=dict(default=True, type='bool'), + description=dict(default=None, type='str'), + ) + module_kwargs = dict( + mutually_exclusive=[ + ['auto_ip', 'floating_ips'], + ['auto_ip', 'floating_ip_pools'], + ['floating_ips', 'floating_ip_pools'], + ['flavor', 'flavor_ram'], + ['image', 'boot_volume'], + ['boot_from_volume', 'boot_volume'], + ['nics', 'network'], + ], + required_if=[ + ('boot_from_volume', True, ['volume_size', 'image']), + ], + ) + + def run(self): + + state = self.params['state'] + image = self.params['image'] + boot_volume = self.params['boot_volume'] + flavor = self.params['flavor'] + flavor_ram = self.params['flavor_ram'] + + if state == 'present': + if not (image or boot_volume): + self.fail( + msg="Parameter 'image' or 'boot_volume' is required " + "if state == 'present'" + ) + if not flavor and not flavor_ram: + self.fail( + msg="Parameter 'flavor' or 'flavor_ram' is required " + "if state == 'present'" + ) + + if state == 'present': + self._get_server_state() + self._create_server() + elif state == 'absent': + self._get_server_state() + self._delete_server() + + def _exit_hostvars(self, server, changed=True): + hostvars = self.conn.get_openstack_vars(server) + self.exit( + changed=changed, server=server, id=server.id, openstack=hostvars) + + def _get_server_state(self): + state = self.params['state'] + server = self.conn.get_server(self.params['name']) + if server and state == 'present': + if server.status not in ('ACTIVE', 'SHUTOFF', 'PAUSED', 'SUSPENDED'): + self.fail( + msg="The instance is available but not Active state: " + server.status) + (ip_changed, server) = self._check_ips(server) + (sg_changed, server) = self._check_security_groups(server) + (server_changed, server) = self._update_server(server) + self._exit_hostvars(server, ip_changed or sg_changed or server_changed) + if server and state == 'absent': + return True + if state == 'absent': + self.exit(changed=False, result="not present") + return True + + def _create_server(self): + flavor = self.params['flavor'] + flavor_ram = self.params['flavor_ram'] + flavor_include = self.params['flavor_include'] + + image_id = None + if not self.params['boot_volume']: + image_id = self.conn.get_image_id( + self.params['image'], self.params['image_exclude']) + if not image_id: + self.fail( + msg="Could not find image %s" % self.params['image']) + + if flavor: + flavor_dict = self.conn.get_flavor(flavor) + if not flavor_dict: + self.fail(msg="Could not find flavor %s" % flavor) + else: + flavor_dict = self.conn.get_flavor_by_ram(flavor_ram, flavor_include) + if not flavor_dict: + self.fail(msg="Could not find any matching flavor") + + nics = self._network_args() + + self.params['meta'] = _parse_meta(self.params['meta']) + + bootkwargs = self.check_versioned( + name=self.params['name'], + image=image_id, + flavor=flavor_dict['id'], + nics=nics, + meta=self.params['meta'], + security_groups=self.params['security_groups'], + userdata=self.params['userdata'], + config_drive=self.params['config_drive'], + ) + for optional_param in ( + 'key_name', 'availability_zone', 'network', + 'scheduler_hints', 'volume_size', 'volumes', + 'description'): + if self.params[optional_param]: + bootkwargs[optional_param] = self.params[optional_param] + + server = self.conn.create_server( + ip_pool=self.params['floating_ip_pools'], + ips=self.params['floating_ips'], + auto_ip=self.params['auto_ip'], + boot_volume=self.params['boot_volume'], + boot_from_volume=self.params['boot_from_volume'], + terminate_volume=self.params['terminate_volume'], + reuse_ips=self.params['reuse_ips'], + wait=self.params['wait'], timeout=self.params['timeout'], + **bootkwargs + ) + + self._exit_hostvars(server) + + def _update_server(self, server): + changed = False + + self.params['meta'] = _parse_meta(self.params['meta']) + + # self.conn.set_server_metadata only updates the key=value pairs, it doesn't + # touch existing ones + update_meta = {} + for (k, v) in self.params['meta'].items(): + if k not in server.metadata or server.metadata[k] != v: + update_meta[k] = v + + if update_meta: + self.conn.set_server_metadata(server, update_meta) + changed = True + # Refresh server vars + server = self.conn.get_server(self.params['name']) + + return (changed, server) + + def _delete_server(self): + try: + self.conn.delete_server( + self.params['name'], wait=self.params['wait'], + timeout=self.params['timeout'], + delete_ips=self.params['delete_fip']) + except Exception as e: + self.fail(msg="Error in deleting vm: %s" % e) + self.exit(changed=True, result='deleted') + + def _network_args(self): + args = [] + nics = self.params['nics'] + + if not isinstance(nics, list): + self.fail(msg='The \'nics\' parameter must be a list.') + + for num, net in enumerate(_parse_nics(nics)): + if not isinstance(net, dict): + self.fail( + msg='Each entry in the \'nics\' parameter must be a dict.') + + if net.get('net-id'): + args.append(net) + elif net.get('net-name'): + by_name = self.conn.get_network(net['net-name']) + if not by_name: + self.fail( + msg='Could not find network by net-name: %s' % + net['net-name']) + resolved_net = net.copy() + del resolved_net['net-name'] + resolved_net['net-id'] = by_name['id'] + args.append(resolved_net) + elif net.get('port-id'): + args.append(net) + elif net.get('port-name'): + by_name = self.conn.get_port(net['port-name']) + if not by_name: + self.fail( + msg='Could not find port by port-name: %s' % + net['port-name']) + resolved_net = net.copy() + del resolved_net['port-name'] + resolved_net['port-id'] = by_name['id'] + args.append(resolved_net) + + if 'tag' in net: + args[num]['tag'] = net['tag'] + return args + + def _detach_ip_list(self, server, extra_ips): + for ip in extra_ips: + ip_id = self.conn.get_floating_ip( + id=None, filters={'floating_ip_address': ip}) + self.conn.detach_ip_from_server( + server_id=server.id, floating_ip_id=ip_id) + + def _check_ips(self, server): + changed = False + + auto_ip = self.params['auto_ip'] + floating_ips = self.params['floating_ips'] + floating_ip_pools = self.params['floating_ip_pools'] + + if floating_ip_pools or floating_ips: + ips = openstack_find_nova_addresses(server.addresses, 'floating') + if not ips: + # If we're configured to have a floating but we don't have one, + # let's add one + server = self.conn.add_ips_to_server( + server, + auto_ip=auto_ip, + ips=floating_ips, + ip_pool=floating_ip_pools, + wait=self.params['wait'], + timeout=self.params['timeout'], + ) + changed = True + elif floating_ips: + # we were configured to have specific ips, let's make sure we have + # those + missing_ips = [] + for ip in floating_ips: + if ip not in ips: + missing_ips.append(ip) + if missing_ips: + server = self.conn.add_ip_list(server, missing_ips, + wait=self.params['wait'], + timeout=self.params['timeout']) + changed = True + extra_ips = [] + for ip in ips: + if ip not in floating_ips: + extra_ips.append(ip) + if extra_ips: + self._detach_ip_list(server, extra_ips) + changed = True + elif auto_ip: + if server['interface_ip']: + changed = False + else: + # We're configured for auto_ip but we're not showing an + # interface_ip. Maybe someone deleted an IP out from under us. + server = self.conn.add_ips_to_server( + server, + auto_ip=auto_ip, + ips=floating_ips, + ip_pool=floating_ip_pools, + wait=self.params['wait'], + timeout=self.params['timeout'], + ) + changed = True + return (changed, server) + + def _check_security_groups(self, server): + changed = False + + # server security groups were added to shade in 1.19. Until then this + # module simply ignored trying to update security groups and only set them + # on newly created hosts. + if not ( + hasattr(self.conn, 'add_server_security_groups') + and hasattr(self.conn, 'remove_server_security_groups') + ): + return changed, server + + module_security_groups = set(self.params['security_groups']) + server_security_groups = set(sg['name'] for sg in server.security_groups) + + add_sgs = module_security_groups - server_security_groups + remove_sgs = server_security_groups - module_security_groups + + if add_sgs: + self.conn.add_server_security_groups(server, list(add_sgs)) + changed = True + + if remove_sgs: + self.conn.remove_server_security_groups(server, list(remove_sgs)) + changed = True + + return (changed, server) + + +def main(): + module = ServerModule() + module() + + +if __name__ == '__main__': + main() |