Skip to content

Commit df7df7e

Browse files
authored
New VM Instance Inventory Plugin (#66)
1 parent 62dc383 commit df7df7e

File tree

12 files changed

+433
-0
lines changed

12 files changed

+433
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
minor_changes:
2+
- instance - New style inventory plugin implemented for instances (https://github.com/ngine-io/ansible-collection-cloudstack/pull/66)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright (c) 2015, René Moser <[email protected]>
4+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5+
6+
from __future__ import absolute_import, division, print_function
7+
__metaclass__ = type
8+
9+
10+
class ModuleDocFragment(object):
11+
12+
# Additional Cloudstack Configuration with Environment Variables Mappings
13+
DOCUMENTATION = r'''
14+
options:
15+
api_key:
16+
env:
17+
- name: CLOUDSTACK_KEY
18+
api_secret:
19+
env:
20+
- name: CLOUDSTACK_SECRET
21+
api_url:
22+
env:
23+
- name: CLOUDSTACK_ENDPOINT
24+
api_http_method:
25+
env:
26+
- name: CLOUDSTACK_METHOD
27+
api_timeout:
28+
env:
29+
- name: CLOUDSTACK_TIMEOUT
30+
api_verify_ssl_cert:
31+
env:
32+
- name: CLOUDSTACK_VERIFY
33+
'''

plugins/inventory/instance.py

+294
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (c) 2020, Rafael del valle <[email protected]>
4+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5+
6+
from __future__ import absolute_import, division, print_function
7+
__metaclass__ = type
8+
9+
import sys
10+
import os
11+
import traceback
12+
import yaml
13+
14+
DOCUMENTATION = r'''
15+
name: instance
16+
plugin_type: inventory
17+
short_description: Apache CloudStack instance inventory source
18+
author: Rafael del Valle (@rvalle)
19+
version_added: 2.1.0
20+
description:
21+
- Get inventory hosts from Apache CloudStack
22+
- Allows filtering and grouping inventory hosts.
23+
- |
24+
Uses an YAML configuration file ending with either I(cloudstack-instances.yml) or I(cloudstack-instances.yaml)
25+
to set parameter values (also see examples).
26+
options:
27+
plugin:
28+
description: Token that ensures this is a source file for the 'instance' plugin.
29+
type: string
30+
required: True
31+
choices: [ ngine_io.cloudstack.instance ]
32+
hostname:
33+
description: |
34+
Field to match the hostname. Note v4_main_ip corresponds to the primary ipv4address of the first nic
35+
adapter of the instance.
36+
type: string
37+
default: v4_default_ip
38+
choices:
39+
- v4_default_ip
40+
- hostname
41+
filter_by_zone:
42+
description: Only return instances in the provided zone.
43+
type: string
44+
filter_by_domain:
45+
description: Only return instances in the provided domain.
46+
type: string
47+
filter_by_project:
48+
description: Only return instances in the provided project.
49+
type: string
50+
filter_by_vpc:
51+
description: Only return instances in the provided VPC.
52+
type: string
53+
extends_documentation_fragment:
54+
- constructed
55+
- ngine_io.cloudstack.cloudstack
56+
- ngine_io.cloudstack.cloudstack_environment
57+
'''
58+
59+
# TODO: plugin should work as 'cloudstack' only
60+
EXAMPLES = '''
61+
# inventory_cloudstack.yml file in YAML format
62+
# Example command line: ansible-inventory --list -i cloudstack-instances.yml
63+
plugin: ngine_io.cloudstack.instance
64+
65+
# Use the default ip as ansible_host
66+
hostname: v4_default_ip
67+
68+
# Return only instances related to the VPC vpc1 and in the zone EU
69+
filter_by_vpc: vpc1
70+
filter_by_zone: EU
71+
72+
# Group instances with a disk_offering as storage
73+
# Create a group dmz for instances connected to the dmz network
74+
groups:
75+
storage: disk_offering is defined
76+
dmz: "'dmz' in networks"
77+
78+
# Group the instances by network, with net_network1 as name of the groups
79+
# Group the instanes by custom tag sla, groups like sla_value for tag sla
80+
keyed_groups:
81+
- prefix: net
82+
key: networks
83+
- prefix: sla
84+
key: tags.sla
85+
86+
87+
'''
88+
89+
# The J2 Template takes 'instance' object as returned from ACS and returns 'instance' object as returned by
90+
# This inventory plugin.
91+
# The data structure of this inventory has been designed according to the following criteria:
92+
# - do not duplicate/compete with Ansible instance facts
93+
# - do not duplicate/compete with Cloudstack facts modules
94+
# - hide internal ACS structures and identifiers
95+
# - if possible use similar naming to previous inventory script
96+
# - prefer non-existing attributes over null values
97+
# - populate the data required to group and filter instances
98+
INVENTORY_NORMALIZATION_J2 = '''
99+
---
100+
instance:
101+
102+
name: {{ instance.name }}
103+
hostname: {{ instance.hostname or instance.name | lower }}
104+
v4_default_ip: {{ instance.nic[0].ipaddress }}
105+
106+
zone: {{ instance.zonename }}
107+
domain: {{ instance.domain | lower }}
108+
account: {{ instance.account }}
109+
username: {{ instance.username }}
110+
{% if instance.group %}
111+
group: {{ instance.group }}
112+
{% endif %}
113+
114+
{% if instance.tags %}
115+
tags:
116+
{% for tag in instance.tags %}
117+
{{ tag.key }}: {{ tag.value }}
118+
{% endfor %}
119+
{% endif %}
120+
121+
template: {{ instance.templatename }}
122+
service_offering: {{ instance.serviceofferingname }}
123+
{% if instance.diskofferingname is defined %}
124+
disk_offering: {{ instance.diskofferingname }}
125+
{% endif %}
126+
{% if instance.affinitygroup %}
127+
affinity_groups:
128+
{% for ag in instance.affinitygroup %}
129+
- {{ ag.name }}
130+
{% endfor %}
131+
{% endif %}
132+
networks:
133+
{% for nic in instance.nic %}
134+
- {{ nic.networkname }}
135+
{% endfor %}
136+
137+
ha_enabled: {{ instance.haenable }}
138+
password_enabled: {{ instance.passwordenabled }}
139+
140+
hypervisor: {{ instance.hypervisor | lower }}
141+
cpu_speed: {{ instance.cpuspeed }}
142+
cpu_number: {{ instance.cpunumber }}
143+
memory: {{ instance.memory }}
144+
dynamically_scalable: {{ instance.isdynamicallyscalable }}
145+
146+
state: {{ instance.state }}
147+
cpu_usage: {{ instance.cpuused }}
148+
created: {{ instance.created }}
149+
'''
150+
151+
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, AnsibleError
152+
from ansible.module_utils.basic import missing_required_lib
153+
from ..module_utils.cloudstack import HAS_LIB_CS
154+
from jinja2 import Template
155+
156+
157+
try:
158+
from cs import CloudStack, CloudStackException
159+
except ImportError:
160+
pass
161+
162+
163+
class InventoryModule(BaseInventoryPlugin, Constructable):
164+
165+
NAME = 'ngine_io.cloudstack.instance'
166+
167+
def __init__(self):
168+
super().__init__()
169+
if not HAS_LIB_CS:
170+
raise AnsibleError(missing_required_lib('cs'))
171+
self._cs = None
172+
self._normalization_template = Template(INVENTORY_NORMALIZATION_J2)
173+
174+
def init_cs(self):
175+
176+
# The configuration logic matches modules specification
177+
api_config = {
178+
'endpoint': self.get_option('api_url'),
179+
'key': self.get_option('api_key'),
180+
'secret': self.get_option('api_secret'),
181+
'timeout': self.get_option('api_timeout'),
182+
'method': self.get_option('api_http_method'),
183+
'verify': self.get_option('api_verify_ssl_cert')
184+
}
185+
186+
self._cs = CloudStack(**api_config)
187+
188+
@property
189+
def cs(self):
190+
return self._cs
191+
192+
def query_api(self, command, **args):
193+
res = getattr(self.cs, command)(**args)
194+
195+
if 'errortext' in res:
196+
raise AnsibleError(res['errortext'])
197+
198+
return res
199+
200+
def verify_file(self, path):
201+
"""return true/false if this is possibly a valid file for this plugin to consume"""
202+
valid = False
203+
if super(InventoryModule, self).verify_file(path):
204+
# base class verifies that file exists and is readable by current user
205+
if path.endswith(('cloudstack-instances.yaml', 'cloudstack-instances.yml')):
206+
valid = True
207+
return valid
208+
209+
def add_filter(self, args, filter_option, query, arg):
210+
# is there a value to filter by? we will search with it
211+
search = self.get_option('filter_by_' + filter_option)
212+
if search:
213+
found = False
214+
# we return all items related to the query involved in the filtering
215+
result = self.query_api(query, listItems=True)
216+
for item in result[filter_option]:
217+
# if we find the searched value as either an id or a name
218+
if search in [item['id'], item['name']]:
219+
# we add the corresponding filter as query argument
220+
args[arg] = item['id']
221+
found = True
222+
if not found:
223+
raise AnsibleError(
224+
"Could not apply filter_by_{fo}. No {fo} with id or name {s} found".format(
225+
fo=filter_option, s=search
226+
)
227+
)
228+
229+
return args
230+
231+
def get_filters(self):
232+
# Filtering as supported by ACS goes here
233+
args = {
234+
'fetch_list': True
235+
}
236+
237+
self.add_filter(args, 'domain', 'listDomains', 'domainid')
238+
self.add_filter(args, 'project', 'listProjects', 'projectid')
239+
self.add_filter(args, 'zone', 'listZones', 'zoneid')
240+
self.add_filter(args, 'vpc', 'listVPCs', 'vpcid')
241+
242+
return args
243+
244+
def normalize_instance_data(self, instance):
245+
inventory_instance_str = self._normalization_template.render(instance=instance)
246+
inventory_instance = yaml.load(inventory_instance_str, Loader=yaml.FullLoader)
247+
return inventory_instance['instance']
248+
249+
def parse(self, inventory, loader, path, cache=False):
250+
251+
# call base method to ensure properties are available for use with other helper methods
252+
super(InventoryModule, self).parse(inventory, loader, path, cache)
253+
254+
# This is the inventory Config
255+
self._read_config_data(path)
256+
257+
# We Initialize the query_api
258+
self.init_cs()
259+
260+
# All Hosts from
261+
self.inventory.add_group('cloudstack')
262+
263+
# The ansible_host preference
264+
hostname_preference = self.get_option('hostname')
265+
266+
# Retrieve the filtered list of instances
267+
instances = self.query_api('listVirtualMachines', **self.get_filters())
268+
269+
for instance in instances:
270+
271+
# we normalize the instance data using the embedded J2 template
272+
instance = self.normalize_instance_data(instance)
273+
274+
inventory_name = instance['name']
275+
self.inventory.add_host(inventory_name, group='cloudstack')
276+
277+
for attribute, value in instance.items():
278+
# Add all available attributes
279+
self.inventory.set_variable(inventory_name, attribute, value)
280+
281+
# set hostname preference
282+
self.inventory.set_variable(inventory_name, 'ansible_host', instance[hostname_preference])
283+
284+
# Use constructed if applicable
285+
strict = self.get_option('strict')
286+
287+
# Composed variables
288+
self._set_composite_vars(self.get_option('compose'), instance, inventory_name, strict=strict)
289+
290+
# Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group
291+
self._add_host_to_composed_groups(self.get_option('groups'), instance, inventory_name, strict=strict)
292+
293+
# Create groups based on variable values and add the corresponding hosts to it
294+
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), instance, inventory_name, strict=strict)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
cloud/cs
2+
shippable/cs/group2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
plugin: ngine_io.cloudstack.cloudstack
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
- hosts: 127.0.0.1
3+
connection: local
4+
gather_facts: no
5+
vars:
6+
simulator: http://cloudstack-sim:8888
7+
tasks:
8+
- name: Retrieve Simulator Keys
9+
uri:
10+
url: "{{simulator}}/admin.json"
11+
return_content: yes
12+
register: admin
13+
14+
- name: Create cloudstack.env
15+
template:
16+
src: templates/cloudstack.env.j2
17+
dest: ../cloudstack.env
18+
19+
- name: Create cloudstack-instances.yml
20+
template:
21+
src: templates/cloudstack-instances.yml.j2
22+
dest: ../cloudstack-instances.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
- hosts: 127.0.0.1
3+
connection: local
4+
gather_facts: no
5+
tasks:
6+
7+
- include_vars:
8+
file: vars/common.yml
9+
10+
- name: wait for system template available
11+
cs_template:
12+
name: "{{ cs_common_template }}"
13+
state: absent
14+
cross_zones: yes
15+
template_filter: all
16+
register: template
17+
check_mode: true
18+
until: template is changed
19+
retries: 20
20+
delay: 5
21+
22+
- name: smoke test instance
23+
cs_instance:
24+
name: smoke-test-vm
25+
template: "{{ cs_common_template }}"
26+
service_offering: "{{ cs_common_service_offering }}"
27+
zone: "{{ cs_common_zone_adv }}"
28+
register: instance
29+
until: instance is successful
30+
retries: 2
31+
delay: 5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
3+
- import_playbook: common-cloudstack-objects.yml

0 commit comments

Comments
 (0)