mirror of
				https://github.com/ansible-collections/community.general.git
				synced 2025-10-26 05:50:36 -07:00 
			
		
		
		
	
		
			Some checks are pending
		
		
	
	EOL CI / EOL Sanity (Ⓐ2.17) (push) Waiting to run
				
			EOL CI / EOL Units (Ⓐ2.17+py3.10) (push) Waiting to run
				
			EOL CI / EOL Units (Ⓐ2.17+py3.12) (push) Waiting to run
				
			EOL CI / EOL Units (Ⓐ2.17+py3.7) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+alpine319+py:azp/posix/1/) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+alpine319+py:azp/posix/2/) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+alpine319+py:azp/posix/3/) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+fedora39+py:azp/posix/1/) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+fedora39+py:azp/posix/2/) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+fedora39+py:azp/posix/3/) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+ubuntu2004+py:azp/posix/1/) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+ubuntu2004+py:azp/posix/2/) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+ubuntu2004+py:azp/posix/3/) (push) Waiting to run
				
			nox / Run extra sanity tests (push) Waiting to run
				
			* Adjust all __future__ imports: for i in $(grep -REl "__future__.*absolute_import" plugins/ tests/); do sed -e 's/from __future__ import .*/from __future__ import annotations/g' -i $i; done * Remove all UTF-8 encoding specifications for Python source files: for i in $(grep -REl '[-][*]- coding: utf-8 -[*]-' plugins/ tests/); do sed -e '/^# -\*- coding: utf-8 -\*-/d' -i $i; done * Remove __metaclass__ = type: for i in $(grep -REl '__metaclass__ = type' plugins/ tests/); do sed -e '/^__metaclass__ = type/d' -i $i; done
		
			
				
	
	
		
			671 lines
		
	
	
	
		
			22 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			671 lines
		
	
	
	
		
			22 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/usr/bin/python
 | |
| # Copyright (c) 2016, Tomas Karasek <tom.to.the.k@gmail.com>
 | |
| # Copyright (c) 2016, Matt Baldwin <baldwin@stackpointcloud.com>
 | |
| # Copyright (c) 2016, Thibaud Morel l'Horset <teebes@gmail.com>
 | |
| # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
 | |
| # SPDX-License-Identifier: GPL-3.0-or-later
 | |
| 
 | |
| from __future__ import annotations
 | |
| 
 | |
| 
 | |
| DOCUMENTATION = r"""
 | |
| module: packet_device
 | |
| 
 | |
| short_description: Manage a bare metal server in the Packet Host
 | |
| 
 | |
| description:
 | |
|   - Manage a bare metal server in the Packet Host (a "device" in the API terms).
 | |
|   - When the machine is created it can optionally wait for public IP address, or for active state.
 | |
|   - This module has a dependency on packet >= 1.0.
 | |
|   - API is documented at U(https://www.packet.net/developers/api/devices).
 | |
| author:
 | |
|   - Tomas Karasek (@t0mk) <tom.to.the.k@gmail.com>
 | |
|   - Matt Baldwin (@baldwinSPC) <baldwin@stackpointcloud.com>
 | |
|   - Thibaud Morel l'Horset (@teebes) <teebes@gmail.com>
 | |
| 
 | |
| extends_documentation_fragment:
 | |
|   - community.general.attributes
 | |
| 
 | |
| attributes:
 | |
|   check_mode:
 | |
|     support: none
 | |
|   diff_mode:
 | |
|     support: none
 | |
| 
 | |
| options:
 | |
|   auth_token:
 | |
|     description:
 | |
|       - Packet API token. You can also supply it in environment variable E(PACKET_API_TOKEN).
 | |
|     type: str
 | |
| 
 | |
|   count:
 | |
|     description:
 | |
|       - The number of devices to create. Count number can be included in hostname using the C(%d) string formatter.
 | |
|     default: 1
 | |
|     type: int
 | |
| 
 | |
|   count_offset:
 | |
|     description:
 | |
|       - From which number to start the count.
 | |
|     default: 1
 | |
|     type: int
 | |
| 
 | |
|   device_ids:
 | |
|     description:
 | |
|       - List of device IDs on which to operate.
 | |
|     type: list
 | |
|     elements: str
 | |
| 
 | |
|   tags:
 | |
|     description:
 | |
|       - List of device tags.
 | |
|       - Currently implemented only for device creation.
 | |
|     type: list
 | |
|     elements: str
 | |
|     version_added: '0.2.0'
 | |
| 
 | |
|   facility:
 | |
|     description:
 | |
|       - Facility slug for device creation. See Packet API for current list - U(https://www.packet.net/developers/api/facilities/).
 | |
|     type: str
 | |
| 
 | |
|   features:
 | |
|     description:
 | |
|       - Dict with "features" for device creation. See Packet API docs for details.
 | |
|     type: dict
 | |
| 
 | |
|   hostnames:
 | |
|     description:
 | |
|       - A hostname of a device, or a list of hostnames.
 | |
|       - If given string or one-item list, you can use the C("%d") Python string format to expand numbers from O(count).
 | |
|       - If only one hostname, it might be expanded to list if O(count)>1.
 | |
|     aliases: [name]
 | |
|     type: list
 | |
|     elements: str
 | |
| 
 | |
|   locked:
 | |
|     description:
 | |
|       - Whether to lock a created device.
 | |
|     default: false
 | |
|     aliases: [lock]
 | |
|     type: bool
 | |
| 
 | |
|   operating_system:
 | |
|     description:
 | |
|       - OS slug for device creation. See Packet API for current list - U(https://www.packet.net/developers/api/operatingsystems/).
 | |
|     type: str
 | |
| 
 | |
|   plan:
 | |
|     description:
 | |
|       - Plan slug for device creation. See Packet API for current list - U(https://www.packet.net/developers/api/plans/).
 | |
|     type: str
 | |
| 
 | |
|   project_id:
 | |
|     description:
 | |
|       - ID of project of the device.
 | |
|     required: true
 | |
|     type: str
 | |
| 
 | |
|   state:
 | |
|     description:
 | |
|       - Desired state of the device.
 | |
|       - If set to V(present) (the default), the module call returns immediately after the device-creating HTTP request successfully
 | |
|         returns.
 | |
|       - If set to V(active), the module call blocks until all the specified devices are in state active due to the Packet
 | |
|         API, or until O(wait_timeout).
 | |
|     choices: [present, absent, active, inactive, rebooted]
 | |
|     default: present
 | |
|     type: str
 | |
| 
 | |
|   user_data:
 | |
|     description:
 | |
|       - Userdata blob made available to the machine.
 | |
|     type: str
 | |
| 
 | |
|   wait_for_public_IPv:
 | |
|     description:
 | |
|       - Whether to wait for the instance to be assigned a public IPv4/IPv6 address.
 | |
|       - If set to V(4), it waits until IPv4 is assigned to the instance.
 | |
|       - If set to V(6), it waits until public IPv6 is assigned to the instance.
 | |
|     choices: [4, 6]
 | |
|     type: int
 | |
| 
 | |
|   wait_timeout:
 | |
|     description:
 | |
|       - How long (seconds) to wait either for automatic IP address assignment, or for the device to reach the V(active) state.
 | |
|       - If O(wait_for_public_IPv) is set and O(state=active), the module waits for both events consequently, applying the
 | |
|         timeout twice.
 | |
|     default: 900
 | |
|     type: int
 | |
| 
 | |
|   ipxe_script_url:
 | |
|     description:
 | |
|       - URL of custom iPXE script for provisioning.
 | |
|       - More about custom iPXE for Packet devices at U(https://help.packet.net/technical/infrastructure/custom-ipxe).
 | |
|     type: str
 | |
|     default: ''
 | |
| 
 | |
|   always_pxe:
 | |
|     description:
 | |
|       - Persist PXE as the first boot option.
 | |
|       - Normally, the PXE process happens only on the first boot. Set this arg to have your device continuously boot to iPXE.
 | |
|     default: false
 | |
|     type: bool
 | |
| 
 | |
| 
 | |
| requirements:
 | |
|   - "packet-python >= 1.35"
 | |
| """
 | |
| 
 | |
| EXAMPLES = r"""
 | |
| # All the examples assume that you have your Packet API token in environment variable PACKET_API_TOKEN.
 | |
| # You can also pass it to the auth_token parameter of the module instead.
 | |
| 
 | |
| # Creating devices
 | |
| 
 | |
| - name: Create 1 device
 | |
|   hosts: localhost
 | |
|   tasks:
 | |
|     - community.general.packet_device:
 | |
|         project_id: 89b497ee-5afc-420a-8fb5-56984898f4df
 | |
|         hostnames: myserver
 | |
|         tags: ci-xyz
 | |
|         operating_system: ubuntu_16_04
 | |
|         plan: baremetal_0
 | |
|         facility: sjc1
 | |
| 
 | |
| # Create the same device and wait until it is in state "active", (when it is
 | |
| # ready for other API operations). Fail if the device is not "active" in
 | |
| # 10 minutes.
 | |
| 
 | |
| - name: Create device and wait up to 10 minutes for active state
 | |
|   hosts: localhost
 | |
|   tasks:
 | |
|     - community.general.packet_device:
 | |
|         project_id: 89b497ee-5afc-420a-8fb5-56984898f4df
 | |
|         hostnames: myserver
 | |
|         operating_system: ubuntu_16_04
 | |
|         plan: baremetal_0
 | |
|         facility: sjc1
 | |
|         state: active
 | |
|         wait_timeout: 600
 | |
| 
 | |
| - name: Create 3 ubuntu devices called server-01, server-02 and server-03
 | |
|   hosts: localhost
 | |
|   tasks:
 | |
|     - community.general.packet_device:
 | |
|         project_id: 89b497ee-5afc-420a-8fb5-56984898f4df
 | |
|         hostnames: server-%02d
 | |
|         count: 3
 | |
|         operating_system: ubuntu_16_04
 | |
|         plan: baremetal_0
 | |
|         facility: sjc1
 | |
| 
 | |
| - name: Create 3 coreos devices with userdata, wait until they get IPs and then wait for SSH
 | |
|   hosts: localhost
 | |
|   tasks:
 | |
|     - name: Create 3 devices and register their facts
 | |
|       community.general.packet_device:
 | |
|         hostnames: [coreos-one, coreos-two, coreos-three]
 | |
|         operating_system: coreos_stable
 | |
|         plan: baremetal_0
 | |
|         facility: ewr1
 | |
|         locked: true
 | |
|         project_id: 89b497ee-5afc-420a-8fb5-56984898f4df
 | |
|         wait_for_public_IPv: 4
 | |
|         user_data: |
 | |
|           #cloud-config
 | |
|           ssh_authorized_keys:
 | |
|             - {{ lookup('file', 'my_packet_sshkey') }}
 | |
|           coreos:
 | |
|             etcd:
 | |
|               discovery: https://discovery.etcd.io/6a28e078895c5ec737174db2419bb2f3
 | |
|               addr: $private_ipv4:4001
 | |
|               peer-addr: $private_ipv4:7001
 | |
|             fleet:
 | |
|               public-ip: $private_ipv4
 | |
|             units:
 | |
|               - name: etcd.service
 | |
|                 command: start
 | |
|               - name: fleet.service
 | |
|                 command: start
 | |
|       register: newhosts
 | |
| 
 | |
|     - name: Wait for ssh
 | |
|       ansible.builtin.wait_for:
 | |
|         delay: 1
 | |
|         host: "{{ item.public_ipv4 }}"
 | |
|         port: 22
 | |
|         state: started
 | |
|         timeout: 500
 | |
|       with_items: "{{ newhosts.devices }}"
 | |
| 
 | |
| 
 | |
| # Other states of devices
 | |
| 
 | |
| - name: Remove 3 devices by uuid
 | |
|   hosts: localhost
 | |
|   tasks:
 | |
|     - community.general.packet_device:
 | |
|         project_id: 89b497ee-5afc-420a-8fb5-56984898f4df
 | |
|         state: absent
 | |
|         device_ids:
 | |
|           - 1fb4faf8-a638-4ac7-8f47-86fe514c30d8
 | |
|           - 2eb4faf8-a638-4ac7-8f47-86fe514c3043
 | |
|           - 6bb4faf8-a638-4ac7-8f47-86fe514c301f
 | |
| """
 | |
| 
 | |
| RETURN = r"""
 | |
| devices:
 | |
|   description: Information about each device that was processed.
 | |
|   type: list
 | |
|   sample:
 | |
|     - "hostname": "my-server.com"
 | |
|       "id": "2a5122b9-c323-4d5c-b53c-9ad3f54273e7"
 | |
|       "public_ipv4": "147.229.15.12"
 | |
|       "private-ipv4": "10.0.15.12"
 | |
|       "tags": []
 | |
|       "locked": false
 | |
|       "state": "provisioning"
 | |
|       "public_ipv6": "2604:1380:2:5200::3"
 | |
|   returned: success
 | |
| """
 | |
| 
 | |
| 
 | |
| import os
 | |
| import re
 | |
| import time
 | |
| import uuid
 | |
| import traceback
 | |
| 
 | |
| from ansible.module_utils.basic import AnsibleModule
 | |
| from ansible.module_utils.common.text.converters import to_native
 | |
| 
 | |
| HAS_PACKET_SDK = True
 | |
| try:
 | |
|     import packet
 | |
| except ImportError:
 | |
|     HAS_PACKET_SDK = False
 | |
| 
 | |
| 
 | |
| NAME_RE = r'({0}|{0}{1}*{0})'.format(r'[a-zA-Z0-9]', r'[a-zA-Z0-9\-]')
 | |
| HOSTNAME_RE = r'({0}\.)*{0}$'.format(NAME_RE)
 | |
| MAX_DEVICES = 100
 | |
| 
 | |
| PACKET_DEVICE_STATES = (
 | |
|     'queued',
 | |
|     'provisioning',
 | |
|     'failed',
 | |
|     'powering_on',
 | |
|     'active',
 | |
|     'powering_off',
 | |
|     'inactive',
 | |
|     'rebooting',
 | |
| )
 | |
| 
 | |
| PACKET_API_TOKEN_ENV_VAR = "PACKET_API_TOKEN"
 | |
| 
 | |
| 
 | |
| ALLOWED_STATES = ['absent', 'active', 'inactive', 'rebooted', 'present']
 | |
| 
 | |
| 
 | |
| def serialize_device(device):
 | |
|     """
 | |
|     Standard representation for a device as returned by various tasks::
 | |
| 
 | |
|         {
 | |
|             'id': 'device_id'
 | |
|             'hostname': 'device_hostname',
 | |
|             'tags': [],
 | |
|             'locked': false,
 | |
|             'state': 'provisioning',
 | |
|             'ip_addresses': [
 | |
|                 {
 | |
|                     "address": "147.75.194.227",
 | |
|                     "address_family": 4,
 | |
|                     "public": true
 | |
|                 },
 | |
|                 {
 | |
|                     "address": "2604:1380:2:5200::3",
 | |
|                     "address_family": 6,
 | |
|                     "public": true
 | |
|                 },
 | |
|                 {
 | |
|                     "address": "10.100.11.129",
 | |
|                     "address_family": 4,
 | |
|                     "public": false
 | |
|                 }
 | |
|             ],
 | |
|             "private_ipv4": "10.100.11.129",
 | |
|             "public_ipv4": "147.75.194.227",
 | |
|             "public_ipv6": "2604:1380:2:5200::3",
 | |
|         }
 | |
| 
 | |
|     """
 | |
|     device_data = {}
 | |
|     device_data['id'] = device.id
 | |
|     device_data['hostname'] = device.hostname
 | |
|     device_data['tags'] = device.tags
 | |
|     device_data['locked'] = device.locked
 | |
|     device_data['state'] = device.state
 | |
|     device_data['ip_addresses'] = [
 | |
|         {
 | |
|             'address': addr_data['address'],
 | |
|             'address_family': addr_data['address_family'],
 | |
|             'public': addr_data['public'],
 | |
|         }
 | |
|         for addr_data in device.ip_addresses
 | |
|     ]
 | |
|     # Also include each IPs as a key for easier lookup in roles.
 | |
|     # Key names:
 | |
|     # - public_ipv4
 | |
|     # - public_ipv6
 | |
|     # - private_ipv4
 | |
|     # - private_ipv6 (if there is one)
 | |
|     for ipdata in device_data['ip_addresses']:
 | |
|         if ipdata['public']:
 | |
|             if ipdata['address_family'] == 6:
 | |
|                 device_data['public_ipv6'] = ipdata['address']
 | |
|             elif ipdata['address_family'] == 4:
 | |
|                 device_data['public_ipv4'] = ipdata['address']
 | |
|         elif not ipdata['public']:
 | |
|             if ipdata['address_family'] == 6:
 | |
|                 # Packet doesn't give public ipv6 yet, but maybe one
 | |
|                 # day they will
 | |
|                 device_data['private_ipv6'] = ipdata['address']
 | |
|             elif ipdata['address_family'] == 4:
 | |
|                 device_data['private_ipv4'] = ipdata['address']
 | |
|     return device_data
 | |
| 
 | |
| 
 | |
| def is_valid_hostname(hostname):
 | |
|     return re.match(HOSTNAME_RE, hostname) is not None
 | |
| 
 | |
| 
 | |
| def is_valid_uuid(myuuid):
 | |
|     try:
 | |
|         val = uuid.UUID(myuuid, version=4)
 | |
|     except ValueError:
 | |
|         return False
 | |
|     return str(val) == myuuid
 | |
| 
 | |
| 
 | |
| def listify_string_name_or_id(s):
 | |
|     if ',' in s:
 | |
|         return s.split(',')
 | |
|     else:
 | |
|         return [s]
 | |
| 
 | |
| 
 | |
| def get_hostname_list(module):
 | |
|     # hostname is a list-typed param, so I guess it should return list
 | |
|     # (and it does, in Ansible 2.2.1) but in order to be defensive,
 | |
|     # I keep here the code to convert an eventual string to list
 | |
|     hostnames = module.params.get('hostnames')
 | |
|     count = module.params.get('count')
 | |
|     count_offset = module.params.get('count_offset')
 | |
|     if isinstance(hostnames, str):
 | |
|         hostnames = listify_string_name_or_id(hostnames)
 | |
|     if not isinstance(hostnames, list):
 | |
|         raise Exception("name %s is not convertible to list" % hostnames)
 | |
| 
 | |
|     # at this point, hostnames is a list
 | |
|     hostnames = [h.strip() for h in hostnames]
 | |
| 
 | |
|     if len(hostnames) > 1 and count > 1:
 | |
|         _msg = ("If you set count>1, you should only specify one hostname "
 | |
|                 "with the %d formatter, not a list of hostnames.")
 | |
|         raise Exception(_msg)
 | |
| 
 | |
|     if len(hostnames) == 1 and count > 0:
 | |
|         hostname_spec = hostnames[0]
 | |
|         count_range = range(count_offset, count_offset + count)
 | |
|         if re.search(r"%\d{0,2}d", hostname_spec):
 | |
|             hostnames = [hostname_spec % i for i in count_range]
 | |
|         elif count > 1:
 | |
|             hostname_spec = '%s%%02d' % hostname_spec
 | |
|             hostnames = [hostname_spec % i for i in count_range]
 | |
| 
 | |
|     for hn in hostnames:
 | |
|         if not is_valid_hostname(hn):
 | |
|             raise Exception("Hostname '%s' does not seem to be valid" % hn)
 | |
| 
 | |
|     if len(hostnames) > MAX_DEVICES:
 | |
|         raise Exception("You specified too many hostnames, max is %d" %
 | |
|                         MAX_DEVICES)
 | |
|     return hostnames
 | |
| 
 | |
| 
 | |
| def get_device_id_list(module):
 | |
|     device_ids = module.params.get('device_ids')
 | |
| 
 | |
|     if isinstance(device_ids, str):
 | |
|         device_ids = listify_string_name_or_id(device_ids)
 | |
| 
 | |
|     device_ids = [di.strip() for di in device_ids]
 | |
| 
 | |
|     for di in device_ids:
 | |
|         if not is_valid_uuid(di):
 | |
|             raise Exception("Device ID '%s' does not seem to be valid" % di)
 | |
| 
 | |
|     if len(device_ids) > MAX_DEVICES:
 | |
|         raise Exception("You specified too many devices, max is %d" %
 | |
|                         MAX_DEVICES)
 | |
|     return device_ids
 | |
| 
 | |
| 
 | |
| def create_single_device(module, packet_conn, hostname):
 | |
| 
 | |
|     for param in ('hostnames', 'operating_system', 'plan'):
 | |
|         if not module.params.get(param):
 | |
|             raise Exception("%s parameter is required for new device."
 | |
|                             % param)
 | |
|     project_id = module.params.get('project_id')
 | |
|     plan = module.params.get('plan')
 | |
|     tags = module.params.get('tags')
 | |
|     user_data = module.params.get('user_data')
 | |
|     facility = module.params.get('facility')
 | |
|     operating_system = module.params.get('operating_system')
 | |
|     locked = module.params.get('locked')
 | |
|     ipxe_script_url = module.params.get('ipxe_script_url')
 | |
|     always_pxe = module.params.get('always_pxe')
 | |
|     if operating_system != 'custom_ipxe':
 | |
|         for param in ('ipxe_script_url', 'always_pxe'):
 | |
|             if module.params.get(param):
 | |
|                 raise Exception('%s parameter is not valid for non custom_ipxe operating_system.' % param)
 | |
| 
 | |
|     device = packet_conn.create_device(
 | |
|         project_id=project_id,
 | |
|         hostname=hostname,
 | |
|         tags=tags,
 | |
|         plan=plan,
 | |
|         facility=facility,
 | |
|         operating_system=operating_system,
 | |
|         userdata=user_data,
 | |
|         locked=locked,
 | |
|         ipxe_script_url=ipxe_script_url,
 | |
|         always_pxe=always_pxe)
 | |
|     return device
 | |
| 
 | |
| 
 | |
| def refresh_device_list(module, packet_conn, devices):
 | |
|     device_ids = [d.id for d in devices]
 | |
|     new_device_list = get_existing_devices(module, packet_conn)
 | |
|     return [d for d in new_device_list if d.id in device_ids]
 | |
| 
 | |
| 
 | |
| def wait_for_devices_active(module, packet_conn, watched_devices):
 | |
|     wait_timeout = module.params.get('wait_timeout')
 | |
|     wait_timeout = time.time() + wait_timeout
 | |
|     refreshed = watched_devices
 | |
|     while wait_timeout > time.time():
 | |
|         refreshed = refresh_device_list(module, packet_conn, watched_devices)
 | |
|         if all(d.state == 'active' for d in refreshed):
 | |
|             return refreshed
 | |
|         time.sleep(5)
 | |
|     raise Exception("Waiting for state \"active\" timed out for devices: %s"
 | |
|                     % [d.hostname for d in refreshed if d.state != "active"])
 | |
| 
 | |
| 
 | |
| def wait_for_public_IPv(module, packet_conn, created_devices):
 | |
| 
 | |
|     def has_public_ip(addr_list, ip_v):
 | |
|         return any(a['public'] and a['address_family'] == ip_v and a['address'] for a in addr_list)
 | |
| 
 | |
|     def all_have_public_ip(ds, ip_v):
 | |
|         return all(has_public_ip(d.ip_addresses, ip_v) for d in ds)
 | |
| 
 | |
|     address_family = module.params.get('wait_for_public_IPv')
 | |
| 
 | |
|     wait_timeout = module.params.get('wait_timeout')
 | |
|     wait_timeout = time.time() + wait_timeout
 | |
|     while wait_timeout > time.time():
 | |
|         refreshed = refresh_device_list(module, packet_conn, created_devices)
 | |
|         if all_have_public_ip(refreshed, address_family):
 | |
|             return refreshed
 | |
|         time.sleep(5)
 | |
| 
 | |
|     raise Exception("Waiting for IPv%d address timed out. Hostnames: %s"
 | |
|                     % (address_family, [d.hostname for d in created_devices]))
 | |
| 
 | |
| 
 | |
| def get_existing_devices(module, packet_conn):
 | |
|     project_id = module.params.get('project_id')
 | |
|     return packet_conn.list_devices(
 | |
|         project_id, params={
 | |
|             'per_page': MAX_DEVICES})
 | |
| 
 | |
| 
 | |
| def get_specified_device_identifiers(module):
 | |
|     if module.params.get('device_ids'):
 | |
|         device_id_list = get_device_id_list(module)
 | |
|         return {'ids': device_id_list, 'hostnames': []}
 | |
|     elif module.params.get('hostnames'):
 | |
|         hostname_list = get_hostname_list(module)
 | |
|         return {'hostnames': hostname_list, 'ids': []}
 | |
| 
 | |
| 
 | |
| def act_on_devices(module, packet_conn, target_state):
 | |
|     specified_identifiers = get_specified_device_identifiers(module)
 | |
|     existing_devices = get_existing_devices(module, packet_conn)
 | |
|     changed = False
 | |
|     create_hostnames = []
 | |
|     if target_state in ['present', 'active', 'rebooted']:
 | |
|         # states where we might create non-existing specified devices
 | |
|         existing_devices_names = [ed.hostname for ed in existing_devices]
 | |
|         create_hostnames = [hn for hn in specified_identifiers['hostnames']
 | |
|                             if hn not in existing_devices_names]
 | |
| 
 | |
|     process_devices = [d for d in existing_devices
 | |
|                        if (d.id in specified_identifiers['ids']) or
 | |
|                        (d.hostname in specified_identifiers['hostnames'])]
 | |
| 
 | |
|     if target_state != 'present':
 | |
|         _absent_state_map = {}
 | |
|         for s in PACKET_DEVICE_STATES:
 | |
|             _absent_state_map[s] = packet.Device.delete
 | |
| 
 | |
|         state_map = {
 | |
|             'absent': _absent_state_map,
 | |
|             'active': {'inactive': packet.Device.power_on,
 | |
|                        'provisioning': None, 'rebooting': None
 | |
|                        },
 | |
|             'inactive': {'active': packet.Device.power_off},
 | |
|             'rebooted': {'active': packet.Device.reboot,
 | |
|                          'inactive': packet.Device.power_on,
 | |
|                          'provisioning': None, 'rebooting': None
 | |
|                          },
 | |
|         }
 | |
| 
 | |
|         # First do non-creation actions, it might be faster
 | |
|         for d in process_devices:
 | |
|             if d.state == target_state:
 | |
|                 continue
 | |
|             if d.state in state_map[target_state]:
 | |
|                 api_operation = state_map[target_state].get(d.state)
 | |
|                 if api_operation is not None:
 | |
|                     api_operation(d)
 | |
|                     changed = True
 | |
|             else:
 | |
|                 _msg = (
 | |
|                     "I don't know how to process existing device %s from state %s "
 | |
|                     "to state %s" %
 | |
|                     (d.hostname, d.state, target_state))
 | |
|                 raise Exception(_msg)
 | |
| 
 | |
|     # At last create missing devices
 | |
|     created_devices = []
 | |
|     if create_hostnames:
 | |
|         created_devices = [create_single_device(module, packet_conn, n)
 | |
|                            for n in create_hostnames]
 | |
|         if module.params.get('wait_for_public_IPv'):
 | |
|             created_devices = wait_for_public_IPv(
 | |
|                 module, packet_conn, created_devices)
 | |
|         changed = True
 | |
| 
 | |
|     processed_devices = created_devices + process_devices
 | |
|     if target_state == 'active':
 | |
|         processed_devices = wait_for_devices_active(
 | |
|             module, packet_conn, processed_devices)
 | |
| 
 | |
|     return {
 | |
|         'changed': changed,
 | |
|         'devices': [serialize_device(d) for d in processed_devices]
 | |
|     }
 | |
| 
 | |
| 
 | |
| def main():
 | |
|     module = AnsibleModule(
 | |
|         argument_spec=dict(
 | |
|             auth_token=dict(default=os.environ.get(PACKET_API_TOKEN_ENV_VAR),
 | |
|                             no_log=True),
 | |
|             count=dict(type='int', default=1),
 | |
|             count_offset=dict(type='int', default=1),
 | |
|             device_ids=dict(type='list', elements='str'),
 | |
|             facility=dict(),
 | |
|             features=dict(type='dict'),
 | |
|             hostnames=dict(type='list', elements='str', aliases=['name']),
 | |
|             tags=dict(type='list', elements='str'),
 | |
|             locked=dict(type='bool', default=False, aliases=['lock']),
 | |
|             operating_system=dict(),
 | |
|             plan=dict(),
 | |
|             project_id=dict(required=True),
 | |
|             state=dict(choices=ALLOWED_STATES, default='present'),
 | |
|             user_data=dict(),
 | |
|             wait_for_public_IPv=dict(type='int', choices=[4, 6]),
 | |
|             wait_timeout=dict(type='int', default=900),
 | |
|             ipxe_script_url=dict(default=''),
 | |
|             always_pxe=dict(type='bool', default=False),
 | |
|         ),
 | |
|         required_one_of=[('device_ids', 'hostnames',)],
 | |
|         mutually_exclusive=[
 | |
|             ('hostnames', 'device_ids'),
 | |
|             ('count', 'device_ids'),
 | |
|             ('count_offset', 'device_ids'),
 | |
|         ]
 | |
|     )
 | |
| 
 | |
|     if not HAS_PACKET_SDK:
 | |
|         module.fail_json(msg='packet required for this module')
 | |
| 
 | |
|     if not module.params.get('auth_token'):
 | |
|         _fail_msg = ("if Packet API token is not in environment variable %s, "
 | |
|                      "the auth_token parameter is required" %
 | |
|                      PACKET_API_TOKEN_ENV_VAR)
 | |
|         module.fail_json(msg=_fail_msg)
 | |
| 
 | |
|     auth_token = module.params.get('auth_token')
 | |
| 
 | |
|     packet_conn = packet.Manager(auth_token=auth_token)
 | |
| 
 | |
|     state = module.params.get('state')
 | |
| 
 | |
|     try:
 | |
|         module.exit_json(**act_on_devices(module, packet_conn, state))
 | |
|     except Exception as e:
 | |
|         module.fail_json(msg='failed to set device state %s, error: %s' %
 | |
|                          (state, to_native(e)), exception=traceback.format_exc())
 | |
| 
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|     main()
 |