mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-04-07 12:37:17 +00:00
Initial commit
This commit is contained in:
commit
aebc1b03fd
4861 changed files with 812621 additions and 0 deletions
643
plugins/modules/cloud/packet/packet_device.py
Normal file
643
plugins/modules/cloud/packet/packet_device.py
Normal file
|
|
@ -0,0 +1,643 @@
|
|||
#!/usr/bin/python
|
||||
# (c) 2016, Tomas Karasek <tom.to.the.k@gmail.com>
|
||||
# (c) 2016, Matt Baldwin <baldwin@stackpointcloud.com>
|
||||
# (c) 2016, Thibaud Morel l'Horset <teebes@gmail.com>
|
||||
#
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
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>
|
||||
|
||||
options:
|
||||
auth_token:
|
||||
description:
|
||||
- Packet api token. You can also supply it in env var C(PACKET_API_TOKEN).
|
||||
|
||||
count:
|
||||
description:
|
||||
- The number of devices to create. Count number can be included in hostname via the %d string formatter.
|
||||
default: 1
|
||||
|
||||
count_offset:
|
||||
description:
|
||||
- From which number to start the count.
|
||||
default: 1
|
||||
|
||||
device_ids:
|
||||
description:
|
||||
- List of device IDs on which to operate.
|
||||
|
||||
facility:
|
||||
description:
|
||||
- Facility slug for device creation. See Packet API for current list - U(https://www.packet.net/developers/api/facilities/).
|
||||
|
||||
features:
|
||||
description:
|
||||
- Dict with "features" for device creation. See Packet API docs for details.
|
||||
|
||||
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 I(count).
|
||||
- If only one hostname, it might be expanded to list if I(count)>1.
|
||||
aliases: [name]
|
||||
|
||||
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/).
|
||||
|
||||
plan:
|
||||
description:
|
||||
- Plan slug for device creation. See Packet API for current list - U(https://www.packet.net/developers/api/plans/).
|
||||
|
||||
project_id:
|
||||
description:
|
||||
- ID of project of the device.
|
||||
required: true
|
||||
|
||||
state:
|
||||
description:
|
||||
- Desired state of the device.
|
||||
- If set to C(present) (the default), the module call will return immediately after the device-creating HTTP request successfully returns.
|
||||
- If set to C(active), the module call will block until all the specified devices are in state active due to the Packet API, or until I(wait_timeout).
|
||||
choices: [present, absent, active, inactive, rebooted]
|
||||
default: present
|
||||
|
||||
user_data:
|
||||
description:
|
||||
- Userdata blob made available to the machine
|
||||
|
||||
wait_for_public_IPv:
|
||||
description:
|
||||
- Whether to wait for the instance to be assigned a public IPv4/IPv6 address.
|
||||
- If set to 4, it will wait until IPv4 is assigned to the instance.
|
||||
- If set to 6, wait until public IPv6 is assigned to the instance.
|
||||
choices: [4,6]
|
||||
|
||||
wait_timeout:
|
||||
description:
|
||||
- How long (seconds) to wait either for automatic IP address assignment, or for the device to reach the C(active) I(state).
|
||||
- If I(wait_for_public_IPv) is set and I(state) is C(active), the module will wait for both events consequently, applying the timeout twice.
|
||||
default: 900
|
||||
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).
|
||||
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"
|
||||
|
||||
notes:
|
||||
- Doesn't support check mode.
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# All the examples assume that you have your Packet api token in env var 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:
|
||||
- packet_device:
|
||||
project_id: 89b497ee-5afc-420a-8fb5-56984898f4df
|
||||
hostnames: myserver
|
||||
operating_system: ubuntu_16_04
|
||||
plan: baremetal_0
|
||||
facility: sjc1
|
||||
|
||||
# Create the same device and wait until it is in state "active", (when it's
|
||||
# ready for other API operations). Fail if the devices in not "active" in
|
||||
# 10 minutes.
|
||||
|
||||
- name: create device and wait up to 10 minutes for active state
|
||||
hosts: localhost
|
||||
tasks:
|
||||
- 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:
|
||||
- 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
|
||||
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
|
||||
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:
|
||||
- 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 = '''
|
||||
changed:
|
||||
description: True if a device was altered in any way (created, modified or removed)
|
||||
type: bool
|
||||
sample: True
|
||||
returned: success
|
||||
|
||||
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
|
||||
''' # NOQA
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
HAS_PACKET_SDK = True
|
||||
try:
|
||||
import packet
|
||||
except ImportError:
|
||||
HAS_PACKET_SDK = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
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')
|
||||
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,
|
||||
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'),
|
||||
facility=dict(),
|
||||
features=dict(type='dict'),
|
||||
hostnames=dict(type='list', aliases=['name']),
|
||||
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(default=None),
|
||||
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()
|
||||
329
plugins/modules/cloud/packet/packet_ip_subnet.py
Normal file
329
plugins/modules/cloud/packet/packet_ip_subnet.py
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2019, Nurfet Becirevic <nurfet.becirevic@gmail.com>
|
||||
# Copyright: (c) 2017, Tomas Karasek <tom.to.the.k@gmail.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: packet_ip_subnet
|
||||
|
||||
short_description: Assign IP subnet to a bare metal server.
|
||||
|
||||
description:
|
||||
- Assign or unassign IPv4 or IPv6 subnets to or from a device in the Packet host.
|
||||
- IPv4 subnets must come from already reserved block.
|
||||
- IPv6 subnets must come from publicly routable /56 block from your project.
|
||||
- See U(https://support.packet.com/kb/articles/elastic-ips) for more info on IP block reservation.
|
||||
|
||||
|
||||
author:
|
||||
- Tomas Karasek (@t0mk) <tom.to.the.k@gmail.com>
|
||||
- Nurfet Becirevic (@nurfet-becirevic) <nurfet.becirevic@gmail.com>
|
||||
|
||||
options:
|
||||
auth_token:
|
||||
description:
|
||||
- Packet api token. You can also supply it in env var C(PACKET_API_TOKEN).
|
||||
type: str
|
||||
|
||||
hostname:
|
||||
description:
|
||||
- A hostname of a device to/from which to assign/remove a subnet.
|
||||
required: False
|
||||
type: str
|
||||
|
||||
device_id:
|
||||
description:
|
||||
- UUID of a device to/from which to assign/remove a subnet.
|
||||
required: False
|
||||
type: str
|
||||
|
||||
project_id:
|
||||
description:
|
||||
- UUID of a project of the device to/from which to assign/remove a subnet.
|
||||
required: True
|
||||
type: str
|
||||
|
||||
device_count:
|
||||
description:
|
||||
- The number of devices to retrieve from the project. The max allowed value is 1000.
|
||||
- See U(https://www.packet.com/developers/api/#retrieve-all-devices-of-a-project) for more info.
|
||||
default: 100
|
||||
type: int
|
||||
|
||||
cidr:
|
||||
description:
|
||||
- IPv4 or IPv6 subnet which you want to manage. It must come from a reserved block for your project in the Packet Host.
|
||||
aliases: [name]
|
||||
type: str
|
||||
|
||||
state:
|
||||
description:
|
||||
- Desired state of the IP subnet on the specified device.
|
||||
- With state == C(present), you must specify either hostname or device_id. Subnet with given CIDR will then be assigned to the specified device.
|
||||
- With state == C(absent), you can specify either hostname or device_id. The subnet will be removed from specified devices.
|
||||
- If you leave both hostname and device_id empty, the subnet will be removed from any device it's assigned to.
|
||||
choices: ['present', 'absent']
|
||||
default: 'present'
|
||||
type: str
|
||||
|
||||
requirements:
|
||||
- "packet-python >= 1.35"
|
||||
- "python >= 2.6"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# All the examples assume that you have your Packet api token in env var PACKET_API_TOKEN.
|
||||
# You can also pass it to the auth_token parameter of the module instead.
|
||||
|
||||
- name: create 1 device and assign an arbitrary public IPv4 subnet to it
|
||||
hosts: localhost
|
||||
tasks:
|
||||
|
||||
- packet_device:
|
||||
project_id: 89b497ee-5afc-420a-8fb5-56984898f4df
|
||||
hostnames: myserver
|
||||
operating_system: ubuntu_16_04
|
||||
plan: baremetal_0
|
||||
facility: sjc1
|
||||
state: active
|
||||
|
||||
# Pick an IPv4 address from a block allocated to your project.
|
||||
|
||||
- packet_ip_subnet:
|
||||
project_id: 89b497ee-5afc-420a-8fb5-56984898f4df
|
||||
hostname: myserver
|
||||
cidr: "147.75.201.78/32"
|
||||
|
||||
# Release IP address 147.75.201.78
|
||||
|
||||
- name: unassign IP address from any device in your project
|
||||
hosts: localhost
|
||||
tasks:
|
||||
- packet_ip_subnet:
|
||||
project_id: 89b497ee-5afc-420a-8fb5-56984898f4df
|
||||
cidr: "147.75.201.78/32"
|
||||
state: absent
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
changed:
|
||||
description: True if an IP address assignments were altered in any way (created or removed).
|
||||
type: bool
|
||||
sample: True
|
||||
returned: success
|
||||
|
||||
device_id:
|
||||
type: str
|
||||
description: UUID of the device associated with the specified IP address.
|
||||
returned: success
|
||||
|
||||
subnet:
|
||||
description: Dict with data about the handled IP subnet.
|
||||
type: dict
|
||||
sample:
|
||||
address: 147.75.90.241
|
||||
address_family: 4
|
||||
assigned_to: { href : /devices/61f9aa5e-0530-47f5-97c2-113828e61ed0 }
|
||||
cidr: 31
|
||||
created_at: '2017-08-07T15:15:30Z'
|
||||
enabled: True
|
||||
gateway: 147.75.90.240
|
||||
href: /ips/31eda960-0a16-4c0f-b196-f3dc4928529f
|
||||
id: 1eda960-0a16-4c0f-b196-f3dc4928529f
|
||||
manageable: True
|
||||
management: True
|
||||
netmask: 255.255.255.254
|
||||
network: 147.75.90.240
|
||||
public: True
|
||||
returned: success
|
||||
'''
|
||||
|
||||
|
||||
import uuid
|
||||
import re
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
||||
from ansible.module_utils._text 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)
|
||||
PROJECT_MAX_DEVICES = 100
|
||||
|
||||
|
||||
PACKET_API_TOKEN_ENV_VAR = "PACKET_API_TOKEN"
|
||||
|
||||
|
||||
ALLOWED_STATES = ['absent', 'present']
|
||||
|
||||
|
||||
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 get_existing_devices(module, packet_conn):
|
||||
project_id = module.params.get('project_id')
|
||||
if not is_valid_uuid(project_id):
|
||||
raise Exception("Project ID {0} does not seem to be valid".format(project_id))
|
||||
|
||||
per_page = module.params.get('device_count')
|
||||
return packet_conn.list_devices(
|
||||
project_id, params={'per_page': per_page})
|
||||
|
||||
|
||||
def get_specified_device_identifiers(module):
|
||||
if module.params.get('device_id'):
|
||||
_d_id = module.params.get('device_id')
|
||||
if not is_valid_uuid(_d_id):
|
||||
raise Exception("Device ID '{0}' does not seem to be valid".format(_d_id))
|
||||
return {'device_id': _d_id, 'hostname': None}
|
||||
elif module.params.get('hostname'):
|
||||
_hn = module.params.get('hostname')
|
||||
if not is_valid_hostname(_hn):
|
||||
raise Exception("Hostname '{0}' does not seem to be valid".format(_hn))
|
||||
return {'hostname': _hn, 'device_id': None}
|
||||
else:
|
||||
return {'hostname': None, 'device_id': None}
|
||||
|
||||
|
||||
def parse_subnet_cidr(cidr):
|
||||
if "/" not in cidr:
|
||||
raise Exception("CIDR expression in wrong format, must be address/prefix_len")
|
||||
addr, prefixlen = cidr.split("/")
|
||||
try:
|
||||
prefixlen = int(prefixlen)
|
||||
except ValueError:
|
||||
raise("Wrong prefix length in CIDR expression {0}".format(cidr))
|
||||
return addr, prefixlen
|
||||
|
||||
|
||||
def act_on_assignment(target_state, module, packet_conn):
|
||||
return_dict = {'changed': False}
|
||||
specified_cidr = module.params.get("cidr")
|
||||
address, prefixlen = parse_subnet_cidr(specified_cidr)
|
||||
|
||||
specified_identifier = get_specified_device_identifiers(module)
|
||||
|
||||
if module.check_mode:
|
||||
return return_dict
|
||||
|
||||
if (specified_identifier['hostname'] is None) and (
|
||||
specified_identifier['device_id'] is None):
|
||||
if target_state == 'absent':
|
||||
# The special case to release the IP from any assignment
|
||||
for d in get_existing_devices(module, packet_conn):
|
||||
for ia in d.ip_addresses:
|
||||
if address == ia['address'] and prefixlen == ia['cidr']:
|
||||
packet_conn.call_api(ia['href'], "DELETE")
|
||||
return_dict['changed'] = True
|
||||
return_dict['subnet'] = ia
|
||||
return_dict['device_id'] = d.id
|
||||
return return_dict
|
||||
raise Exception("If you assign an address, you must specify either "
|
||||
"target device ID or target unique hostname.")
|
||||
|
||||
if specified_identifier['device_id'] is not None:
|
||||
device = packet_conn.get_device(specified_identifier['device_id'])
|
||||
else:
|
||||
all_devices = get_existing_devices(module, packet_conn)
|
||||
hn = specified_identifier['hostname']
|
||||
matching_devices = [d for d in all_devices if d.hostname == hn]
|
||||
if len(matching_devices) > 1:
|
||||
raise Exception("There are more than one devices matching given hostname {0}".format(hn))
|
||||
if len(matching_devices) == 0:
|
||||
raise Exception("There is no device matching given hostname {0}".format(hn))
|
||||
device = matching_devices[0]
|
||||
|
||||
return_dict['device_id'] = device.id
|
||||
assignment_dicts = [i for i in device.ip_addresses
|
||||
if i['address'] == address and i['cidr'] == prefixlen]
|
||||
if len(assignment_dicts) > 1:
|
||||
raise Exception("IP address {0} is assigned more than once for device {1}".format(
|
||||
specified_cidr, device.hostname))
|
||||
|
||||
if target_state == "absent":
|
||||
if len(assignment_dicts) == 1:
|
||||
packet_conn.call_api(assignment_dicts[0]['href'], "DELETE")
|
||||
return_dict['subnet'] = assignment_dicts[0]
|
||||
return_dict['changed'] = True
|
||||
elif target_state == "present":
|
||||
if len(assignment_dicts) == 0:
|
||||
new_assignment = packet_conn.call_api(
|
||||
"devices/{0}/ips".format(device.id), "POST", {"address": "{0}".format(specified_cidr)})
|
||||
return_dict['changed'] = True
|
||||
return_dict['subnet'] = new_assignment
|
||||
return return_dict
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
auth_token=dict(
|
||||
type='str',
|
||||
fallback=(env_fallback, [PACKET_API_TOKEN_ENV_VAR]),
|
||||
no_log=True
|
||||
),
|
||||
device_id=dict(type='str'),
|
||||
hostname=dict(type='str'),
|
||||
project_id=dict(type='str'),
|
||||
device_count=dict(type='int', default=PROJECT_MAX_DEVICES),
|
||||
cidr=dict(type='str', required=True, aliases=['name']),
|
||||
state=dict(choices=ALLOWED_STATES, default='present'),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
mutually_exclusive=[('hostname', 'device_id')],
|
||||
required_one_of=[['hostname', 'device_id', 'project_id']],
|
||||
required_by=dict(
|
||||
hostname=('project_id',),
|
||||
),
|
||||
)
|
||||
|
||||
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 {0}, "
|
||||
"the auth_token parameter is required".format(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_assignment(state, module, packet_conn))
|
||||
except Exception as e:
|
||||
module.fail_json(
|
||||
msg="failed to set IP subnet to state {0}, error: {1}".format(state, to_native(e)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
247
plugins/modules/cloud/packet/packet_project.py
Normal file
247
plugins/modules/cloud/packet/packet_project.py
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2019, Nurfet Becirevic <nurfet.becirevic@gmail.com>
|
||||
# Copyright: (c) 2019, Tomas Karasek <tom.to.the.k@gmail.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: packet_project
|
||||
|
||||
short_description: Create/delete a project in Packet host.
|
||||
|
||||
description:
|
||||
- Create/delete a project in Packet host.
|
||||
- API is documented at U(https://www.packet.com/developers/api/#projects).
|
||||
|
||||
|
||||
author:
|
||||
- Tomas Karasek (@t0mk) <tom.to.the.k@gmail.com>
|
||||
- Nurfet Becirevic (@nurfet-becirevic) <nurfet.becirevic@gmail.com>
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the target.
|
||||
default: present
|
||||
choices: ['present', 'absent']
|
||||
type: str
|
||||
|
||||
payment_method:
|
||||
description:
|
||||
- Payment method is name of one of the payment methods available to your user.
|
||||
- When blank, the API assumes the default payment method.
|
||||
type: str
|
||||
|
||||
auth_token:
|
||||
description:
|
||||
- Packet api token. You can also supply it in env var C(PACKET_API_TOKEN).
|
||||
type: str
|
||||
|
||||
name:
|
||||
description:
|
||||
- Name for/of the project.
|
||||
type: str
|
||||
|
||||
org_id:
|
||||
description:
|
||||
- UUID of the organization to create a project for.
|
||||
- When blank, the API assumes the default organization.
|
||||
type: str
|
||||
|
||||
id:
|
||||
description:
|
||||
- UUID of the project which you want to remove.
|
||||
type: str
|
||||
|
||||
custom_data:
|
||||
description:
|
||||
- Custom data about the project to create.
|
||||
type: str
|
||||
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "packet-python >= 1.40"
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# All the examples assume that you have your Packet API token in env var PACKET_API_TOKEN.
|
||||
# You can also pass the api token in module param auth_token.
|
||||
|
||||
- name: create new project
|
||||
hosts: localhost
|
||||
tasks:
|
||||
packet_project:
|
||||
name: "new project"
|
||||
|
||||
- name: create new project within non-default organization
|
||||
hosts: localhost
|
||||
tasks:
|
||||
packet_project:
|
||||
name: "my org project"
|
||||
org_id: a4cc87f9-e00f-48c2-9460-74aa60beb6b0
|
||||
|
||||
- name: remove project by id
|
||||
hosts: localhost
|
||||
tasks:
|
||||
packet_project:
|
||||
state: absent
|
||||
id: eef49903-7a09-4ca1-af67-4087c29ab5b6
|
||||
|
||||
- name: create new project with non-default billing method
|
||||
hosts: localhost
|
||||
tasks:
|
||||
packet_project:
|
||||
name: "newer project"
|
||||
payment_method: "the other visa"
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
changed:
|
||||
description: True if a project was created or removed.
|
||||
type: bool
|
||||
sample: True
|
||||
returned: success
|
||||
|
||||
name:
|
||||
description: Name of addressed project.
|
||||
type: str
|
||||
returned: success
|
||||
|
||||
id:
|
||||
description: UUID of addressed project.
|
||||
type: str
|
||||
returned: success
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
HAS_PACKET_SDK = True
|
||||
|
||||
try:
|
||||
import packet
|
||||
except ImportError:
|
||||
HAS_PACKET_SDK = False
|
||||
|
||||
PACKET_API_TOKEN_ENV_VAR = "PACKET_API_TOKEN"
|
||||
|
||||
|
||||
def act_on_project(target_state, module, packet_conn):
|
||||
result_dict = {'changed': False}
|
||||
given_id = module.params.get('id')
|
||||
given_name = module.params.get('name')
|
||||
if given_id:
|
||||
matching_projects = [
|
||||
p for p in packet_conn.list_projects() if given_id == p.id]
|
||||
else:
|
||||
matching_projects = [
|
||||
p for p in packet_conn.list_projects() if given_name == p.name]
|
||||
|
||||
if target_state == 'present':
|
||||
if len(matching_projects) == 0:
|
||||
org_id = module.params.get('org_id')
|
||||
custom_data = module.params.get('custom_data')
|
||||
payment_method = module.params.get('payment_method')
|
||||
|
||||
if not org_id:
|
||||
params = {
|
||||
"name": given_name,
|
||||
"payment_method_id": payment_method,
|
||||
"customdata": custom_data
|
||||
}
|
||||
new_project_data = packet_conn.call_api("projects", "POST", params)
|
||||
new_project = packet.Project(new_project_data, packet_conn)
|
||||
else:
|
||||
new_project = packet_conn.create_organization_project(
|
||||
org_id=org_id,
|
||||
name=given_name,
|
||||
payment_method_id=payment_method,
|
||||
customdata=custom_data
|
||||
)
|
||||
|
||||
result_dict['changed'] = True
|
||||
matching_projects.append(new_project)
|
||||
|
||||
result_dict['name'] = matching_projects[0].name
|
||||
result_dict['id'] = matching_projects[0].id
|
||||
else:
|
||||
if len(matching_projects) > 1:
|
||||
_msg = ("More than projects matched for module call with state = absent: "
|
||||
"{0}".format(to_native(matching_projects)))
|
||||
module.fail_json(msg=_msg)
|
||||
|
||||
if len(matching_projects) == 1:
|
||||
p = matching_projects[0]
|
||||
result_dict['name'] = p.name
|
||||
result_dict['id'] = p.id
|
||||
result_dict['changed'] = True
|
||||
try:
|
||||
p.delete()
|
||||
except Exception as e:
|
||||
_msg = ("while trying to remove project {0}, id {1}, got error: {2}".format(
|
||||
p.name, p.id, to_native(e)))
|
||||
module.fail_json(msg=_msg)
|
||||
return result_dict
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
state=dict(choices=['present', 'absent'], default='present'),
|
||||
auth_token=dict(
|
||||
type='str',
|
||||
fallback=(env_fallback, [PACKET_API_TOKEN_ENV_VAR]),
|
||||
no_log=True
|
||||
),
|
||||
name=dict(type='str'),
|
||||
id=dict(type='str'),
|
||||
org_id=dict(type='str'),
|
||||
payment_method=dict(type='str'),
|
||||
custom_data=dict(type='str'),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
required_one_of=[("name", "id",)],
|
||||
mutually_exclusive=[
|
||||
('name', 'id'),
|
||||
]
|
||||
)
|
||||
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 {0}, "
|
||||
"the auth_token parameter is required".format(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')
|
||||
|
||||
if state in ['present', 'absent']:
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=False)
|
||||
|
||||
try:
|
||||
module.exit_json(**act_on_project(state, module, packet_conn))
|
||||
except Exception as e:
|
||||
module.fail_json(
|
||||
msg="failed to set project state {0}: {1}".format(state, to_native(e)))
|
||||
else:
|
||||
module.fail_json(msg="{0} is not a valid state for this module".format(state))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
266
plugins/modules/cloud/packet/packet_sshkey.py
Normal file
266
plugins/modules/cloud/packet/packet_sshkey.py
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright 2016 Tomas Karasek <tom.to.the.k@gmail.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: packet_sshkey
|
||||
short_description: Create/delete an SSH key in Packet host.
|
||||
description:
|
||||
- Create/delete an SSH key in Packet host.
|
||||
- API is documented at U(https://www.packet.net/help/api/#page:ssh-keys,header:ssh-keys-ssh-keys-post).
|
||||
author: "Tomas Karasek (@t0mk) <tom.to.the.k@gmail.com>"
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the target.
|
||||
default: present
|
||||
choices: ['present', 'absent']
|
||||
auth_token:
|
||||
description:
|
||||
- Packet api token. You can also supply it in env var C(PACKET_API_TOKEN).
|
||||
label:
|
||||
description:
|
||||
- Label for the key. If you keep it empty, it will be read from key string.
|
||||
id:
|
||||
description:
|
||||
- UUID of the key which you want to remove.
|
||||
fingerprint:
|
||||
description:
|
||||
- Fingerprint of the key which you want to remove.
|
||||
key:
|
||||
description:
|
||||
- Public Key string ({type} {base64 encoded key} {description}).
|
||||
key_file:
|
||||
description:
|
||||
- File with the public key.
|
||||
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- packet-python
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# All the examples assume that you have your Packet API token in env var PACKET_API_TOKEN.
|
||||
# You can also pass the api token in module param auth_token.
|
||||
|
||||
- name: create sshkey from string
|
||||
hosts: localhost
|
||||
tasks:
|
||||
packet_sshkey:
|
||||
key: "{{ lookup('file', 'my_packet_sshkey.pub') }}"
|
||||
|
||||
- name: create sshkey from file
|
||||
hosts: localhost
|
||||
tasks:
|
||||
packet_sshkey:
|
||||
label: key from file
|
||||
key_file: ~/ff.pub
|
||||
|
||||
- name: remove sshkey by id
|
||||
hosts: localhost
|
||||
tasks:
|
||||
packet_sshkey:
|
||||
state: absent
|
||||
id: eef49903-7a09-4ca1-af67-4087c29ab5b6
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
changed:
|
||||
description: True if a sshkey was created or removed.
|
||||
type: bool
|
||||
sample: True
|
||||
returned: always
|
||||
sshkeys:
|
||||
description: Information about sshkeys that were createe/removed.
|
||||
type: list
|
||||
sample: [
|
||||
{
|
||||
"fingerprint": "5c:93:74:7c:ed:07:17:62:28:75:79:23:d6:08:93:46",
|
||||
"id": "41d61bd8-3342-428b-a09c-e67bdd18a9b7",
|
||||
"key": "ssh-dss AAAAB3NzaC1kc3MAAACBAIfNT5S0ncP4BBJBYNhNPxFF9lqVhfPeu6SM1LoCocxqDc1AT3zFRi8hjIf6TLZ2AA4FYbcAWxLMhiBxZRVldT9GdBXile78kAK5z3bKTwq152DCqpxwwbaTIggLFhsU8wrfBsPWnDuAxZ0h7mmrCjoLIE3CNLDA/NmV3iB8xMThAAAAFQCStcesSgR1adPORzBxTr7hug92LwAAAIBOProm3Gk+HWedLyE8IfofLaOeRnbBRHAOL4z0SexKkVOnQ/LGN/uDIIPGGBDYTvXgKZT+jbHeulRJ2jKgfSpGKN4JxFQ8uzVH492jEiiUJtT72Ss1dCV4PmyERVIw+f54itihV3z/t25dWgowhb0int8iC/OY3cGodlmYb3wdcQAAAIBuLbB45djZXzUkOTzzcRDIRfhaxo5WipbtEM2B1fuBt2gyrvksPpH/LK6xTjdIIb0CxPu4OCxwJG0aOz5kJoRnOWIXQGhH7VowrJhsqhIc8gN9ErbO5ea8b1L76MNcAotmBDeTUiPw01IJ8MdDxfmcsCslJKgoRKSmQpCwXQtN2g== tomk@hp2",
|
||||
"label": "mynewkey33"
|
||||
}
|
||||
]
|
||||
returned: always
|
||||
''' # NOQA
|
||||
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
HAS_PACKET_SDK = True
|
||||
try:
|
||||
import packet
|
||||
except ImportError:
|
||||
HAS_PACKET_SDK = False
|
||||
|
||||
|
||||
PACKET_API_TOKEN_ENV_VAR = "PACKET_API_TOKEN"
|
||||
|
||||
|
||||
def serialize_sshkey(sshkey):
|
||||
sshkey_data = {}
|
||||
copy_keys = ['id', 'key', 'label', 'fingerprint']
|
||||
for name in copy_keys:
|
||||
sshkey_data[name] = getattr(sshkey, name)
|
||||
return sshkey_data
|
||||
|
||||
|
||||
def is_valid_uuid(myuuid):
|
||||
try:
|
||||
val = uuid.UUID(myuuid, version=4)
|
||||
except ValueError:
|
||||
return False
|
||||
return str(val) == myuuid
|
||||
|
||||
|
||||
def load_key_string(key_str):
|
||||
ret_dict = {}
|
||||
key_str = key_str.strip()
|
||||
ret_dict['key'] = key_str
|
||||
cut_key = key_str.split()
|
||||
if len(cut_key) in [2, 3]:
|
||||
if len(cut_key) == 3:
|
||||
ret_dict['label'] = cut_key[2]
|
||||
else:
|
||||
raise Exception("Public key %s is in wrong format" % key_str)
|
||||
return ret_dict
|
||||
|
||||
|
||||
def get_sshkey_selector(module):
|
||||
key_id = module.params.get('id')
|
||||
if key_id:
|
||||
if not is_valid_uuid(key_id):
|
||||
raise Exception("sshkey ID %s is not valid UUID" % key_id)
|
||||
selecting_fields = ['label', 'fingerprint', 'id', 'key']
|
||||
select_dict = {}
|
||||
for f in selecting_fields:
|
||||
if module.params.get(f) is not None:
|
||||
select_dict[f] = module.params.get(f)
|
||||
|
||||
if module.params.get('key_file'):
|
||||
with open(module.params.get('key_file')) as _file:
|
||||
loaded_key = load_key_string(_file.read())
|
||||
select_dict['key'] = loaded_key['key']
|
||||
if module.params.get('label') is None:
|
||||
if loaded_key.get('label'):
|
||||
select_dict['label'] = loaded_key['label']
|
||||
|
||||
def selector(k):
|
||||
if 'key' in select_dict:
|
||||
# if key string is specified, compare only the key strings
|
||||
return k.key == select_dict['key']
|
||||
else:
|
||||
# if key string not specified, all the fields must match
|
||||
return all([select_dict[f] == getattr(k, f) for f in select_dict])
|
||||
return selector
|
||||
|
||||
|
||||
def act_on_sshkeys(target_state, module, packet_conn):
|
||||
selector = get_sshkey_selector(module)
|
||||
existing_sshkeys = packet_conn.list_ssh_keys()
|
||||
matching_sshkeys = filter(selector, existing_sshkeys)
|
||||
changed = False
|
||||
if target_state == 'present':
|
||||
if matching_sshkeys == []:
|
||||
# there is no key matching the fields from module call
|
||||
# => create the key, label and
|
||||
newkey = {}
|
||||
if module.params.get('key_file'):
|
||||
with open(module.params.get('key_file')) as f:
|
||||
newkey = load_key_string(f.read())
|
||||
if module.params.get('key'):
|
||||
newkey = load_key_string(module.params.get('key'))
|
||||
if module.params.get('label'):
|
||||
newkey['label'] = module.params.get('label')
|
||||
for param in ('label', 'key'):
|
||||
if param not in newkey:
|
||||
_msg = ("If you want to ensure a key is present, you must "
|
||||
"supply both a label and a key string, either in "
|
||||
"module params, or in a key file. %s is missing"
|
||||
% param)
|
||||
raise Exception(_msg)
|
||||
matching_sshkeys = []
|
||||
new_key_response = packet_conn.create_ssh_key(
|
||||
newkey['label'], newkey['key'])
|
||||
changed = True
|
||||
|
||||
matching_sshkeys.append(new_key_response)
|
||||
else:
|
||||
# state is 'absent' => delete matching keys
|
||||
for k in matching_sshkeys:
|
||||
try:
|
||||
k.delete()
|
||||
changed = True
|
||||
except Exception as e:
|
||||
_msg = ("while trying to remove sshkey %s, id %s %s, "
|
||||
"got error: %s" %
|
||||
(k.label, k.id, target_state, e))
|
||||
raise Exception(_msg)
|
||||
|
||||
return {
|
||||
'changed': changed,
|
||||
'sshkeys': [serialize_sshkey(k) for k in matching_sshkeys]
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
state=dict(choices=['present', 'absent'], default='present'),
|
||||
auth_token=dict(default=os.environ.get(PACKET_API_TOKEN_ENV_VAR),
|
||||
no_log=True),
|
||||
label=dict(type='str', aliases=['name'], default=None),
|
||||
id=dict(type='str', default=None),
|
||||
fingerprint=dict(type='str', default=None),
|
||||
key=dict(type='str', default=None, no_log=True),
|
||||
key_file=dict(type='path', default=None),
|
||||
),
|
||||
mutually_exclusive=[
|
||||
('label', 'id'),
|
||||
('label', 'fingerprint'),
|
||||
('id', 'fingerprint'),
|
||||
('key', 'fingerprint'),
|
||||
('key', 'id'),
|
||||
('key_file', 'key'),
|
||||
]
|
||||
)
|
||||
|
||||
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')
|
||||
|
||||
if state in ['present', 'absent']:
|
||||
try:
|
||||
module.exit_json(**act_on_sshkeys(state, module, packet_conn))
|
||||
except Exception as e:
|
||||
module.fail_json(msg='failed to set sshkey state: %s' % str(e))
|
||||
else:
|
||||
module.fail_json(msg='%s is not a valid state for this module' % state)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
325
plugins/modules/cloud/packet/packet_volume.py
Normal file
325
plugins/modules/cloud/packet/packet_volume.py
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2019, Nurfet Becirevic <nurfet.becirevic@gmail.com>
|
||||
# Copyright: (c) 2017, Tomas Karasek <tom.to.the.k@gmail.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: packet_volume
|
||||
|
||||
short_description: Create/delete a volume in Packet host.
|
||||
|
||||
description:
|
||||
- Create/delete a volume in Packet host.
|
||||
- API is documented at U(https://www.packet.com/developers/api/#volumes).
|
||||
|
||||
|
||||
author:
|
||||
- Tomas Karasek (@t0mk) <tom.to.the.k@gmail.com>
|
||||
- Nurfet Becirevic (@nurfet-becirevic) <nurfet.becirevic@gmail.com>
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Desired state of the volume.
|
||||
default: present
|
||||
choices: ['present', 'absent']
|
||||
type: str
|
||||
|
||||
project_id:
|
||||
description:
|
||||
- ID of project of the device.
|
||||
required: true
|
||||
type: str
|
||||
|
||||
auth_token:
|
||||
description:
|
||||
- Packet api token. You can also supply it in env var C(PACKET_API_TOKEN).
|
||||
type: str
|
||||
|
||||
name:
|
||||
description:
|
||||
- Selector for API-generated name of the volume
|
||||
type: str
|
||||
|
||||
description:
|
||||
description:
|
||||
- User-defined description attribute for Packet volume.
|
||||
- "It is used used as idempotent identifier - if volume with given
|
||||
description exists, new one is not created."
|
||||
type: str
|
||||
|
||||
id:
|
||||
description:
|
||||
- UUID of a volume.
|
||||
type: str
|
||||
|
||||
plan:
|
||||
description:
|
||||
- storage_1 for standard tier, storage_2 for premium (performance) tier.
|
||||
- Tiers are described at U(https://www.packet.com/cloud/storage/).
|
||||
choices: ['storage_1', 'storage_2']
|
||||
default: 'storage_1'
|
||||
type: str
|
||||
|
||||
facility:
|
||||
description:
|
||||
- Location of the volume.
|
||||
- Volumes can only be attached to device in the same location.
|
||||
type: str
|
||||
|
||||
size:
|
||||
description:
|
||||
- Size of the volume in gigabytes.
|
||||
type: int
|
||||
|
||||
locked:
|
||||
description:
|
||||
- Create new volume locked.
|
||||
type: bool
|
||||
default: False
|
||||
|
||||
billing_cycle:
|
||||
description:
|
||||
- Billing cycle for new volume.
|
||||
choices: ['hourly', 'monthly']
|
||||
default: 'hourly'
|
||||
type: str
|
||||
|
||||
snapshot_policy:
|
||||
description:
|
||||
- Snapshot policy for new volume.
|
||||
type: dict
|
||||
|
||||
suboptions:
|
||||
snapshot_count:
|
||||
description:
|
||||
- How many snapshots to keep, a positive integer.
|
||||
required: True
|
||||
type: int
|
||||
|
||||
snapshot_frequency:
|
||||
description:
|
||||
- Frequency of snapshots.
|
||||
required: True
|
||||
choices: ["15min", "1hour", "1day", "1week", "1month", "1year"]
|
||||
type: str
|
||||
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "packet-python >= 1.35"
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# All the examples assume that you have your Packet API token in env var PACKET_API_TOKEN.
|
||||
# You can also pass the api token in module param auth_token.
|
||||
|
||||
- hosts: localhost
|
||||
vars:
|
||||
volname: testvol123
|
||||
project_id: 53000fb2-ee46-4673-93a8-de2c2bdba33b
|
||||
|
||||
tasks:
|
||||
- name: test create volume
|
||||
packet_volume:
|
||||
description: "{{ volname }}"
|
||||
project_id: "{{ project_id }}"
|
||||
facility: 'ewr1'
|
||||
plan: 'storage_1'
|
||||
state: present
|
||||
size: 10
|
||||
snapshot_policy:
|
||||
snapshot_count: 10
|
||||
snapshot_frequency: 1day
|
||||
register: result_create
|
||||
|
||||
- name: test delete volume
|
||||
packet_volume:
|
||||
id: "{{ result_create.id }}"
|
||||
project_id: "{{ project_id }}"
|
||||
state: absent
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
id:
|
||||
description: UUID of specified volume
|
||||
type: str
|
||||
returned: success
|
||||
sample: 53000fb2-ee46-4673-93a8-de2c2bdba33c
|
||||
name:
|
||||
description: The API-generated name of the volume resource.
|
||||
type: str
|
||||
returned: if volume is attached/detached to/from some device
|
||||
sample: "volume-a91dc506"
|
||||
description:
|
||||
description: The user-defined description of the volume resource.
|
||||
type: str
|
||||
returned: success
|
||||
sample: "Just another volume"
|
||||
'''
|
||||
|
||||
import uuid
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
HAS_PACKET_SDK = True
|
||||
|
||||
|
||||
try:
|
||||
import packet
|
||||
except ImportError:
|
||||
HAS_PACKET_SDK = False
|
||||
|
||||
|
||||
PACKET_API_TOKEN_ENV_VAR = "PACKET_API_TOKEN"
|
||||
|
||||
VOLUME_PLANS = ["storage_1", "storage_2"]
|
||||
VOLUME_STATES = ["present", "absent"]
|
||||
BILLING = ["hourly", "monthly"]
|
||||
|
||||
|
||||
def is_valid_uuid(myuuid):
|
||||
try:
|
||||
val = uuid.UUID(myuuid, version=4)
|
||||
except ValueError:
|
||||
return False
|
||||
return str(val) == myuuid
|
||||
|
||||
|
||||
def get_volume_selector(module):
|
||||
if module.params.get('id'):
|
||||
i = module.params.get('id')
|
||||
if not is_valid_uuid(i):
|
||||
raise Exception("Volume ID '{0}' is not a valid UUID".format(i))
|
||||
return lambda v: v['id'] == i
|
||||
elif module.params.get('name'):
|
||||
n = module.params.get('name')
|
||||
return lambda v: v['name'] == n
|
||||
elif module.params.get('description'):
|
||||
d = module.params.get('description')
|
||||
return lambda v: v['description'] == d
|
||||
|
||||
|
||||
def get_or_fail(params, key):
|
||||
item = params.get(key)
|
||||
if item is None:
|
||||
raise Exception("{0} must be specified for new volume".format(key))
|
||||
return item
|
||||
|
||||
|
||||
def act_on_volume(target_state, module, packet_conn):
|
||||
return_dict = {'changed': False}
|
||||
s = get_volume_selector(module)
|
||||
project_id = module.params.get("project_id")
|
||||
api_method = "projects/{0}/storage".format(project_id)
|
||||
all_volumes = packet_conn.call_api(api_method, "GET")['volumes']
|
||||
matching_volumes = [v for v in all_volumes if s(v)]
|
||||
|
||||
if target_state == "present":
|
||||
if len(matching_volumes) == 0:
|
||||
params = {
|
||||
"description": get_or_fail(module.params, "description"),
|
||||
"size": get_or_fail(module.params, "size"),
|
||||
"plan": get_or_fail(module.params, "plan"),
|
||||
"facility": get_or_fail(module.params, "facility"),
|
||||
"locked": get_or_fail(module.params, "locked"),
|
||||
"billing_cycle": get_or_fail(module.params, "billing_cycle"),
|
||||
"snapshot_policies": module.params.get("snapshot_policy"),
|
||||
}
|
||||
|
||||
new_volume_data = packet_conn.call_api(api_method, "POST", params)
|
||||
return_dict['changed'] = True
|
||||
for k in ['id', 'name', 'description']:
|
||||
return_dict[k] = new_volume_data[k]
|
||||
|
||||
else:
|
||||
for k in ['id', 'name', 'description']:
|
||||
return_dict[k] = matching_volumes[0][k]
|
||||
|
||||
else:
|
||||
if len(matching_volumes) > 1:
|
||||
_msg = ("More than one volume matches in module call for absent state: {0}".format(
|
||||
to_native(matching_volumes)))
|
||||
module.fail_json(msg=_msg)
|
||||
|
||||
if len(matching_volumes) == 1:
|
||||
volume = matching_volumes[0]
|
||||
packet_conn.call_api("storage/{0}".format(volume['id']), "DELETE")
|
||||
return_dict['changed'] = True
|
||||
for k in ['id', 'name', 'description']:
|
||||
return_dict[k] = volume[k]
|
||||
|
||||
return return_dict
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
id=dict(type='str', default=None),
|
||||
description=dict(type="str", default=None),
|
||||
name=dict(type='str', default=None),
|
||||
state=dict(choices=VOLUME_STATES, default="present"),
|
||||
auth_token=dict(
|
||||
type='str',
|
||||
fallback=(env_fallback, [PACKET_API_TOKEN_ENV_VAR]),
|
||||
no_log=True
|
||||
),
|
||||
project_id=dict(required=True),
|
||||
plan=dict(choices=VOLUME_PLANS, default="storage_1"),
|
||||
facility=dict(type="str"),
|
||||
size=dict(type="int"),
|
||||
locked=dict(type="bool", default=False),
|
||||
snapshot_policy=dict(type='dict', default=None),
|
||||
billing_cycle=dict(type='str', choices=BILLING, default="hourly"),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
required_one_of=[("name", "id", "description")],
|
||||
mutually_exclusive=[
|
||||
('name', 'id'),
|
||||
('id', 'description'),
|
||||
('name', 'description'),
|
||||
]
|
||||
)
|
||||
|
||||
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 {0}, "
|
||||
"the auth_token parameter is required".format(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')
|
||||
|
||||
if state in VOLUME_STATES:
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=False)
|
||||
|
||||
try:
|
||||
module.exit_json(**act_on_volume(state, module, packet_conn))
|
||||
except Exception as e:
|
||||
module.fail_json(
|
||||
msg="failed to set volume state {0}: {1}".format(
|
||||
state, to_native(e)))
|
||||
else:
|
||||
module.fail_json(msg="{0} is not a valid state for this module".format(state))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
301
plugins/modules/cloud/packet/packet_volume_attachment.py
Normal file
301
plugins/modules/cloud/packet/packet_volume_attachment.py
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2019, Nurfet Becirevic <nurfet.becirevic@gmail.com>
|
||||
# Copyright: (c) 2017, Tomas Karasek <tom.to.the.k@gmail.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: packet_volume_attachment
|
||||
|
||||
short_description: Attach/detach a volume to a device in the Packet host.
|
||||
|
||||
description:
|
||||
- Attach/detach a volume to a device in the Packet host.
|
||||
- API is documented at U(https://www.packet.com/developers/api/volumes/).
|
||||
- "This module creates the attachment route in the Packet API. In order to discover
|
||||
the block devices on the server, you have to run the Attach Scripts,
|
||||
as documented at U(https://help.packet.net/technical/storage/packet-block-storage-linux)."
|
||||
|
||||
|
||||
author:
|
||||
- Tomas Karasek (@t0mk) <tom.to.the.k@gmail.com>
|
||||
- Nurfet Becirevic (@nurfet-becirevic) <nurfet.becirevic@gmail.com>
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the attachment.
|
||||
default: present
|
||||
choices: ['present', 'absent']
|
||||
type: str
|
||||
|
||||
auth_token:
|
||||
description:
|
||||
- Packet api token. You can also supply it in env var C(PACKET_API_TOKEN).
|
||||
type: str
|
||||
|
||||
project_id:
|
||||
description:
|
||||
- UUID of the project to which the device and volume belong.
|
||||
type: str
|
||||
|
||||
volume:
|
||||
description:
|
||||
- Selector for the volume.
|
||||
- It can be a UUID, an API-generated volume name, or user-defined description string.
|
||||
- 'Example values: 4a347482-b546-4f67-8300-fb5018ef0c5, volume-4a347482, "my volume"'
|
||||
type: str
|
||||
|
||||
device:
|
||||
description:
|
||||
- Selector for the device.
|
||||
- It can be a UUID of the device, or a hostname.
|
||||
- 'Example values: 98a14f7a-3d27-4478-b7cf-35b5670523f3, "my device"'
|
||||
type: str
|
||||
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "packet-python >= 1.35"
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# All the examples assume that you have your Packet API token in env var PACKET_API_TOKEN.
|
||||
# You can also pass the api token in module param auth_token.
|
||||
|
||||
- hosts: localhost
|
||||
|
||||
vars:
|
||||
volname: testvol
|
||||
devname: testdev
|
||||
project_id: 52000fb2-ee46-4673-93a8-de2c2bdba33b
|
||||
|
||||
tasks:
|
||||
- name: test create volume
|
||||
packet_volume:
|
||||
description: "{{ volname }}"
|
||||
project_id: "{{ project_id }}"
|
||||
facility: ewr1
|
||||
plan: storage_1
|
||||
state: present
|
||||
size: 10
|
||||
snapshot_policy:
|
||||
snapshot_count: 10
|
||||
snapshot_frequency: 1day
|
||||
|
||||
- packet_device:
|
||||
project_id: "{{ project_id }}"
|
||||
hostnames: "{{ devname }}"
|
||||
operating_system: ubuntu_16_04
|
||||
plan: baremetal_0
|
||||
facility: ewr1
|
||||
state: present
|
||||
|
||||
- name: Attach testvol to testdev
|
||||
packet_volume_attachment:
|
||||
project_id: "{{ project_id }}"
|
||||
volume: "{{ volname }}"
|
||||
device: "{{ devname }}"
|
||||
|
||||
- name: Detach testvol from testdev
|
||||
packet_volume_attachment:
|
||||
project_id: "{{ project_id }}"
|
||||
volume: "{{ volname }}"
|
||||
device: "{{ devname }}"
|
||||
state: absent
|
||||
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
volume_id:
|
||||
description: UUID of volume addressed by the module call.
|
||||
type: str
|
||||
returned: success
|
||||
|
||||
device_id:
|
||||
description: UUID of device addressed by the module call.
|
||||
type: str
|
||||
returned: success
|
||||
'''
|
||||
|
||||
import uuid
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
HAS_PACKET_SDK = True
|
||||
|
||||
|
||||
try:
|
||||
import packet
|
||||
except ImportError:
|
||||
HAS_PACKET_SDK = False
|
||||
|
||||
|
||||
PACKET_API_TOKEN_ENV_VAR = "PACKET_API_TOKEN"
|
||||
|
||||
STATES = ["present", "absent"]
|
||||
|
||||
|
||||
def is_valid_uuid(myuuid):
|
||||
try:
|
||||
val = uuid.UUID(myuuid, version=4)
|
||||
except ValueError:
|
||||
return False
|
||||
return str(val) == myuuid
|
||||
|
||||
|
||||
def get_volume_selector(spec):
|
||||
if is_valid_uuid(spec):
|
||||
return lambda v: v['id'] == spec
|
||||
else:
|
||||
return lambda v: v['name'] == spec or v['description'] == spec
|
||||
|
||||
|
||||
def get_device_selector(spec):
|
||||
if is_valid_uuid(spec):
|
||||
return lambda v: v['id'] == spec
|
||||
else:
|
||||
return lambda v: v['hostname'] == spec
|
||||
|
||||
|
||||
def do_attach(packet_conn, vol_id, dev_id):
|
||||
api_method = "storage/{0}/attachments".format(vol_id)
|
||||
packet_conn.call_api(
|
||||
api_method,
|
||||
params={"device_id": dev_id},
|
||||
type="POST")
|
||||
|
||||
|
||||
def do_detach(packet_conn, vol, dev_id=None):
|
||||
def dev_match(a):
|
||||
return (dev_id is None) or (a['device']['id'] == dev_id)
|
||||
for a in vol['attachments']:
|
||||
if dev_match(a):
|
||||
print(a['href'])
|
||||
packet_conn.call_api(a['href'], type="DELETE")
|
||||
|
||||
|
||||
def validate_selected(l, resource_type, spec):
|
||||
if len(l) > 1:
|
||||
_msg = ("more than one {0} matches specification {1}: {2}".format(
|
||||
resource_type, spec, l))
|
||||
raise Exception(_msg)
|
||||
if len(l) == 0:
|
||||
_msg = "no {0} matches specification: {1}".format(resource_type, spec)
|
||||
raise Exception(_msg)
|
||||
|
||||
|
||||
def get_attached_dev_ids(volume_dict):
|
||||
if len(volume_dict['attachments']) == 0:
|
||||
return []
|
||||
else:
|
||||
return [a['device']['id'] for a in volume_dict['attachments']]
|
||||
|
||||
|
||||
def act_on_volume_attachment(target_state, module, packet_conn):
|
||||
return_dict = {'changed': False}
|
||||
volspec = module.params.get("volume")
|
||||
devspec = module.params.get("device")
|
||||
if devspec is None and target_state == 'present':
|
||||
raise Exception("If you want to attach a volume, you must specify a device.")
|
||||
project_id = module.params.get("project_id")
|
||||
volumes_api_method = "projects/{0}/storage".format(project_id)
|
||||
volumes = packet_conn.call_api(volumes_api_method,
|
||||
params={'include': 'facility,attachments.device'})['volumes']
|
||||
v_match = get_volume_selector(volspec)
|
||||
matching_volumes = [v for v in volumes if v_match(v)]
|
||||
validate_selected(matching_volumes, "volume", volspec)
|
||||
volume = matching_volumes[0]
|
||||
return_dict['volume_id'] = volume['id']
|
||||
|
||||
device = None
|
||||
if devspec is not None:
|
||||
devices_api_method = "projects/{0}/devices".format(project_id)
|
||||
devices = packet_conn.call_api(devices_api_method)['devices']
|
||||
d_match = get_device_selector(devspec)
|
||||
matching_devices = [d for d in devices if d_match(d)]
|
||||
validate_selected(matching_devices, "device", devspec)
|
||||
device = matching_devices[0]
|
||||
return_dict['device_id'] = device['id']
|
||||
|
||||
attached_device_ids = get_attached_dev_ids(volume)
|
||||
|
||||
if target_state == "present":
|
||||
if len(attached_device_ids) == 0:
|
||||
do_attach(packet_conn, volume['id'], device['id'])
|
||||
return_dict['changed'] = True
|
||||
elif device['id'] not in attached_device_ids:
|
||||
# Don't reattach volume which is attached to a different device.
|
||||
# Rather fail than force remove a device on state == 'present'.
|
||||
raise Exception("volume {0} is already attached to device {1}".format(
|
||||
volume, attached_device_ids))
|
||||
else:
|
||||
if device is None:
|
||||
if len(attached_device_ids) > 0:
|
||||
do_detach(packet_conn, volume)
|
||||
return_dict['changed'] = True
|
||||
elif device['id'] in attached_device_ids:
|
||||
do_detach(packet_conn, volume, device['id'])
|
||||
return_dict['changed'] = True
|
||||
|
||||
return return_dict
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
state=dict(choices=STATES, default="present"),
|
||||
auth_token=dict(
|
||||
type='str',
|
||||
fallback=(env_fallback, [PACKET_API_TOKEN_ENV_VAR]),
|
||||
no_log=True
|
||||
),
|
||||
volume=dict(type="str", required=True),
|
||||
project_id=dict(type="str", required=True),
|
||||
device=dict(type="str"),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
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 {0}, "
|
||||
"the auth_token parameter is required".format(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')
|
||||
|
||||
if state in STATES:
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=False)
|
||||
|
||||
try:
|
||||
module.exit_json(
|
||||
**act_on_volume_attachment(state, module, packet_conn))
|
||||
except Exception as e:
|
||||
module.fail_json(
|
||||
msg="failed to set volume_attachment state {0}: {1}".format(state, to_native(e)))
|
||||
else:
|
||||
module.fail_json(msg="{0} is not a valid state for this module".format(state))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue