1
0
Fork 0
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:
Ansible Core Team 2020-03-09 09:11:07 +00:00
commit aebc1b03fd
4861 changed files with 812621 additions and 0 deletions

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()