mirror of https://github.com/ansible/ansible.git
2 modules for Packet host: packet_device and packet_sshkey (#19005)
* Added 2 modules for Packet Host: packet_device and packet_sshkey * Fixed comments from @mmlb * Fixed comments from @gundalow * Fix typos pointed by @gundalow * Mention new Packet modules in the CHANGELOG.mdpull/19555/merge
parent
e6466339e5
commit
3ebbcbadcf
@ -0,0 +1,566 @@
|
|||||||
|
#!/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>
|
||||||
|
#
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: packet_device
|
||||||
|
|
||||||
|
short_description: create, destroy, start, stop, and reboot a Packet Host machine.
|
||||||
|
|
||||||
|
description:
|
||||||
|
- create, destroy, update, start, stop, and reboot a Packet Host machine. When the machine is created it can optionally wait for it to have an IP address before returning. This module has a dependency on packet >= 1.0.
|
||||||
|
- API is documented at U(https://www.packet.net/help/api/#page:devices,header:devices-devices-post).
|
||||||
|
|
||||||
|
version_added: 2.3
|
||||||
|
|
||||||
|
author: Tomas Karasek <tom.to.the.k@gmail.com>, Matt Baldwin <baldwin@stackpointcloud.com>, Thibaud Morel l'Horset <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.
|
||||||
|
|
||||||
|
count_offset:
|
||||||
|
description:
|
||||||
|
- From which number to start the count.
|
||||||
|
|
||||||
|
device_ids:
|
||||||
|
description:
|
||||||
|
- List of device IDs on which to operate.
|
||||||
|
|
||||||
|
facility:
|
||||||
|
description:
|
||||||
|
- Facility slug for device creation. As of 2016, it should be one of [ewr1, sjc1, ams1, nrt1].
|
||||||
|
|
||||||
|
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 count.
|
||||||
|
- If only one hostname, it might be expanded to list if count>1.
|
||||||
|
aliases: [name]
|
||||||
|
|
||||||
|
lock:
|
||||||
|
description:
|
||||||
|
- Whether to lock a created device.
|
||||||
|
default: false
|
||||||
|
|
||||||
|
operating_system:
|
||||||
|
description:
|
||||||
|
- OS slug for device creation. See Packet docs or API for current list.
|
||||||
|
|
||||||
|
plan:
|
||||||
|
description:
|
||||||
|
- Plan slug for device creation. See Packet docs or API for current list.
|
||||||
|
|
||||||
|
project_id:
|
||||||
|
description:
|
||||||
|
- ID of project of the device.
|
||||||
|
required: true
|
||||||
|
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Desired state of the device.
|
||||||
|
choices: [present, absent, active, inactive, rebooted]
|
||||||
|
default: 'present'
|
||||||
|
|
||||||
|
user_data:
|
||||||
|
description:
|
||||||
|
- Userdata blob made available to the machine
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
|
||||||
|
wait:
|
||||||
|
description:
|
||||||
|
- Whether to wait for the instance to be assigned IP address before returning.
|
||||||
|
required: false
|
||||||
|
default: False
|
||||||
|
type: bool
|
||||||
|
|
||||||
|
wait_timeout:
|
||||||
|
description:
|
||||||
|
- How long to wait for IP address of new devices before quitting. In seconds.
|
||||||
|
default: 60
|
||||||
|
|
||||||
|
requirements:
|
||||||
|
- packet-python
|
||||||
|
- "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.
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
- 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
|
||||||
|
user_data: |
|
||||||
|
#cloud-config
|
||||||
|
ssh_authorized_keys:
|
||||||
|
- ssh-dss AAAAB3NzaC1kc3MAAACBAIfNT5S0ncP4BBJBYNhNPxFF9lqVhfPeu6SM1LoCocxqDc1AT3zFRi8hjIf6TLZ2AA4FYbcAWxLMhiBxZRVldT9GdBXile78kAK5z3bKTwq152DCqpxwwbaTIggLFhsU8wrfBsPWnDuAxZ0h7mmrCjoLIE3CNLDA/NmV3iB8xMThAAAAFQCStcesSgR1adPORzBxTr7hug92LwAAAIBOProm3Gk+HWedLyE8IfofLaOeRnbBRHAOL4z0SexKkVOnQ/LGN/uDIIPGGBDYTvXgKZT+jbHeulRJ2jKgfSpGKN4JxFQ8uzVH492jEiiUJtT72Ss1dCV4PmyERVIw+f54itihV3z/t25dWgowhb0int8iC/OY3cGodlmYb3wdcQAAAIBuLbB45djZXzUkOTzzcRDIRfhaxo5WipbtEM2B1fuBt2gyrvksPpH/LK6xTjdIIb0CxPu4OCxwJG0aOz5kJoRnOWIXQGhH7VowrJhsqhIc8gN9ErbO5ea8b1L76MNcAotmBDeTUiPw01IJ8MdDxfmcsCslJKgoRKSmQpCwXQtN2g== tomk@hp2
|
||||||
|
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: always
|
||||||
|
devices:
|
||||||
|
description: Information about each device that was processed
|
||||||
|
type: array
|
||||||
|
sample: '[{"hostname": "my-server.com", "id": "server-id", "public-ipv4": "147.229.15.12", "private-ipv4": "10.0.15.12", "public-ipv6": ""2604:1380:2:5200::3"}]'
|
||||||
|
returned: always
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
import re
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
|
||||||
|
HAS_PACKET_SDK = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
import packet
|
||||||
|
except ImportError:
|
||||||
|
HAS_PACKET_SDK = False
|
||||||
|
|
||||||
|
|
||||||
|
NAME_RE = '({0}|{0}{1}*{0})'.format('[a-zA-Z0-9]','[a-zA-Z0-9\-]')
|
||||||
|
HOSTNAME_RE = '({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 represenation for a device as returned by various tasks::
|
||||||
|
|
||||||
|
{
|
||||||
|
'id': 'device_id'
|
||||||
|
'hostname': 'device_hostname',
|
||||||
|
'tags': [],
|
||||||
|
'locked': false,
|
||||||
|
'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['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 [i for i in 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 Ansbile 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("%\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 devices, 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')
|
||||||
|
locked = module.params.get('lock')
|
||||||
|
operating_system = module.params.get('operating_system')
|
||||||
|
locked = module.params.get('locked')
|
||||||
|
|
||||||
|
device = packet_conn.create_device(
|
||||||
|
project_id=project_id,
|
||||||
|
hostname=hostname,
|
||||||
|
plan=plan,
|
||||||
|
facility=facility,
|
||||||
|
operating_system=operating_system,
|
||||||
|
userdata=user_data,
|
||||||
|
locked=locked)
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_ips(module, packet_conn, created_devices):
|
||||||
|
|
||||||
|
def has_public_ip(addr_list):
|
||||||
|
return any([a['public'] and (len(a['address']) > 0) for a in addr_list])
|
||||||
|
|
||||||
|
def all_have_public_ip(ds):
|
||||||
|
return all([has_public_ip(d.ip_addresses) for d in ds])
|
||||||
|
|
||||||
|
def refresh_created_devices(ids_of_created_devices, module, packet_conn):
|
||||||
|
new_device_list = get_existing_devices(module, packet_conn)
|
||||||
|
return [d for d in new_device_list if d.id in ids_of_created_devices]
|
||||||
|
|
||||||
|
created_ids = [d.id for d in created_devices]
|
||||||
|
wait_timeout = module.params.get('wait_timeout')
|
||||||
|
wait_timeout = time.time() + wait_timeout
|
||||||
|
while wait_timeout > time.time():
|
||||||
|
refreshed = refresh_created_devices(created_ids, module,
|
||||||
|
packet_conn)
|
||||||
|
if all_have_public_ip(refreshed):
|
||||||
|
return refreshed
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
raise Exception("Waiting for IP assignment timed out. Hostnames: %s"
|
||||||
|
% [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(target_state, module, packet_conn):
|
||||||
|
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},
|
||||||
|
'inactive': {'active': packet.Device.power_off},
|
||||||
|
'rebooted': {'active': packet.Device.reboot,
|
||||||
|
'inactive': packet.Device.power_on},
|
||||||
|
}
|
||||||
|
|
||||||
|
# First do non-creation actions, it might be faster
|
||||||
|
for d in process_devices:
|
||||||
|
if d.state in state_map[target_state]:
|
||||||
|
api_operation = state_map[target_state].get(d.state)
|
||||||
|
try:
|
||||||
|
api_operation(d)
|
||||||
|
changed = True
|
||||||
|
except Exception as e:
|
||||||
|
_msg = ("while trying to make device %s, id %s %s, from state %s, "
|
||||||
|
"with api call by %s got error: %s" %
|
||||||
|
(d.hostname, d.id, target_state, d.state, api_operation, e))
|
||||||
|
raise Exception(_msg)
|
||||||
|
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'):
|
||||||
|
created_devices = wait_for_ips(module, packet_conn, created_devices)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
processed_devices = created_devices + process_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(default='ewr1'),
|
||||||
|
features=dict(type='dict'),
|
||||||
|
hostnames=dict(type='list', aliases=['name']),
|
||||||
|
locked=dict(type='bool', default=False),
|
||||||
|
operating_system=dict(),
|
||||||
|
plan=dict(),
|
||||||
|
project_id=dict(required=True),
|
||||||
|
state=dict(choices=ALLOWED_STATES, default='present'),
|
||||||
|
user_data=dict(default=None),
|
||||||
|
wait=dict(type='bool', default=False),
|
||||||
|
wait_timeout=dict(type='int', default=60),
|
||||||
|
|
||||||
|
),
|
||||||
|
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(state, module, packet_conn))
|
||||||
|
except Exception as e:
|
||||||
|
module.fail_json(msg='failed to set machine state %s, error: %s' % (state,str(e)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,274 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# Copyright 2016 Tomas Karasek <tom.to.the.k@gmail.com>
|
||||||
|
#
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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).
|
||||||
|
version_added: "2.3"
|
||||||
|
author: "Tomas Karasek <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: ssh-dss AAAAB3NzaC1kc3MAAACBAIfNT5S0ncP4BBJBYNhNPxFF9lqVhfPeu6SM1LoCocxqDc1AT3zFRi8hjIf6TLZ2AA4FYbcAWxLMhiBxZRVldT9GdBXile78kAK5z3bKTwq152DCqpxwwbaTIggLFhsU8wrfBsPWnDuAxZ0h7mmrCjoLIE3CNLDA/NmV3iB8xMThAAAAFQCStcesSgR1adPORzBxTr7hug92LwAAAIBOProm3Gk+HWedLyE8IfofLaOeRnbBRHAOL4z0SexKkVOnQ/LGN/uDIIPGGBDYTvXgKZT+jbHeulRJ2jKgfSpGKN4JxFQ8uzVH492jEiiUJtT72Ss1dCV4PmyERVIw+f54itihV3z/t25dWgowhb0int8iC/OY3cGodlmYb3wdcQAAAIBuLbB45djZXzUkOTzzcRDIRfhaxo5WipbtEM2B1fuBt2gyrvksPpH/LK6xTjdIIb0CxPu4OCxwJG0aOz5kJoRnOWIXQGhH7VowrJhsqhIc8gN9ErbO5ea8b1L76MNcAotmBDeTUiPw01IJ8MdDxfmcsCslJKgoRKSmQpCwXQtN2g== tomk@hp2
|
||||||
|
|
||||||
|
- 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: array
|
||||||
|
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
|
||||||
|
'''
|
||||||
|
|
||||||
|
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 mathcing 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()
|
Loading…
Reference in New Issue