Skip to content

Commit

Permalink
Merge pull request #7 from serverscom/add_aliases
Browse files Browse the repository at this point in the history
Add sc_l2_segment_aliases module
  • Loading branch information
amarao authored May 23, 2022
2 parents 46e196a + f1a2877 commit 538b089
Show file tree
Hide file tree
Showing 6 changed files with 469 additions and 1 deletion.
1 change: 1 addition & 0 deletions ansible_collections/serverscom/sc_api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,5 @@ List of modules
* `sc_l2_segment_info` - information about L2 segment
* `sc_l2_segments_info` - list of existing L2 segments
* `sc_l2_segment` - Creation/delelition/membership modification for L2 segments
* `sc_l2_segment_aliases` - Adding and removing IP addresses to/from L2 segments

2 changes: 1 addition & 1 deletion ansible_collections/serverscom/sc_api/galaxy.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
namespace: serverscom
name: sc_api
version: 0.0.5
version: 0.0.6
readme: README.md
authors:
- George Shuklin <george.shuklin@gmail.com>
Expand Down
110 changes: 110 additions & 0 deletions ansible_collections/serverscom/sc_api/plugins/module_utils/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -1329,3 +1329,113 @@ def run(self):
return self.absent()
else: # present
return self.present()


class ScL2SegmentAliases():
def __init__(
self, endpoint, token, name, segment_id,
count, aliases_absent,
wait, update_interval, checkmode
):
self.api = ScApi(token, endpoint)
self.name = name
self.segment_id = segment_id
self.count = count
self.aliases_absent = aliases_absent
self.wait = wait
self.update_interval = update_interval
self.checkmode = checkmode

# TODO: code repeated from ScL2Segment
def get_segment_id(self):
existing_segment_id = None
if self.segment_id:
existing_segment_id = self.api.get_l2_segment(self.segment_id)['id']
else:
for segment in self.api.list_l2_segments():
if segment['name'] == self.name:
if existing_segment_id: # duplicate found
raise ModuleError(msg=f"Duplicate segment with name {self.name} found.")
existing_segment_id = segment['id']
if not existing_segment_id:
raise ModuleError(f"Segment { self.name } is not found.")
return existing_segment_id

def wait_for(self, l2):
start_time = time.time()
if not self.wait:
return
while l2['status'] == 'pending':
time.sleep(self.update_interval)
elapsed = time.time() - start_time
if elapsed > self.wait:
raise WaitError(
msg=f"Timeout while waiting for L2 {l2['id']}."
f" Last status was {l2['status']}",
timeout=elapsed
)
l2 = self.api.get_l2_segment(l2['id'])

def prep_result(self, changed):
aliases = list(self.api.list_l2_segment_networks(self.found_segment_id))
ipv4_list = [
alias['cidr'].split('/')[0] for alias in aliases if alias['family'] == 'ipv4'
]
ipv6_list = [
alias['cidr'].split('/')[0] for alias in aliases if alias['family'] == 'ipv6'
]
return {
'changed': changed,
'aliases': aliases,
'aliases_count': len(aliases),
'ipv4_list': ipv4_list,
'ipv6_list': ipv6_list,
'id': self.found_segment_id
}

def add_aliases(self, add_count):
if not self.checkmode:
create_array = [{
'mask': 32,
'distribution_method': 'route'
}] * add_count
res = self.api.put_l2_segment_networks(self.found_segment_id, create=create_array, delete=[])
self.wait_for(res)
return self.prep_result(True)

@staticmethod
def extract_ids(aliases):
return [alias['id'] for alias in aliases]

@staticmethod
def get_del_list(aliases_existing_ids, aliases_absent_ids):
return list(set(aliases_absent_ids) & set(aliases_existing_ids))

def remove_aliases(self, to_remove_ids):
changed = False
del_list = self.get_del_list(self.existing_aliases_id, to_remove_ids)
if del_list:
changed = True
if not self.checkmode:
res = self.api.put_l2_segment_networks(self.found_segment_id, create=[], delete=del_list)
self.wait_for(res)
return self.prep_result(changed)

def set_count(self):
changed = False
if len(self.existing_aliases) < self.count:
return self.add_aliases(self.count - len(self.existing_aliases_id))
elif len(self.existing_aliases) > self.count:
return self.remove_aliases(self.existing_aliases_id[self.count:])
return self.prep_result(changed)

def run(self):
self.found_segment_id = self.get_segment_id()
self.existing_aliases = list(self.api.list_l2_segment_networks(self.found_segment_id))
self.existing_aliases_id = self.extract_ids(self.existing_aliases)
if self.aliases_absent:
return self.remove_aliases(self.aliases_absent)
else:
if self.count is None:
raise ModuleError('Count must not be None')
return self.set_count()
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,19 @@ def list_l2_segment_networks(self, l2_segment_id):
path=f"/l2_segments/{l2_segment_id}/networks"
)

def put_l2_segment_networks(self, l2_segment_id, create, delete):
'''create: object: mask (int), distribution_method: must be "route"'''
body = {
"create": create,
"delete": delete
}
return self.api_helper.make_put_request(
path=f"/l2_segments/{l2_segment_id}/networks",
query_parameters=None,
body=body,
good_codes=[200, 202]
)

def get_l2_segment(self, l2_segment_id):
return self.api_helper.make_get_request(path=f"/l2_segments/{l2_segment_id}")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2020, Servers.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: sc_l2_segment_aliases
version_added: "1.0.0"
author: "George Shuklin (@amarao)"
short_description: Managing L2 segement
description: >
Create/delete L2 segment aliases.
options:
endpoint:
type: str
default: https://api.servers.com/v1
description:
- Endpoint to use to connect to API.
token:
type: str
required: true
description:
- Token to use.
- You can create token for you account in https://portal.servers.com
in Profile -> Public API section.
name:
type: str
description:
- Name of the segment
- This module required to all segments to have unique names
- If two segments have the same name (even in different location groups
or having different type), this module fails
- Either I(name) or I(segment_id) is required for delete/update of
existing segment.
- I(name) is required if new segment need to be created
segment_id:
type: str
description:
- ID of the segment
- Either I(name) or I(segment_id) can be used
count:
type: int
description:
- Number of aliases assigned to L2 segment
- Either I(count) or I(aliases_absent) can be used
- Either I(count) or I(aliases_absent) must be specified
- Value 0 removes all aliases
aliases_absent:
type: list
elements: str
description:
- List of aliases (by IP or ID) to be absent for the segment
- Either I(count) or I(aliases_absent) can be used
- Either I(count) or I(aliases_absent) must be specified
wait:
type: int
default: 1200
description:
- Max wait time for operation to be finished.
- By default waiting for L2 segment to become active.
- C(0) disable waiting (fire-and-forget mode)
update_interval:
type: int
default: 30
description:
- Interval for polling for of L2 segment
- Each update consumes API quota.
- Ignored if I(wait)=C(0)
"""

RETURN = """
id:
description:
- Id of the segment
returned: on success
type: list
aliases:
description:
- List of aliases
returned: on success
type: complex
contains:
id:
description:
- ID of alias
type: str
family:
type: str
description:
- Either ipv4 or ipv6
cidr:
type: str
description:
- IP address of the alias with /32 mask
alias_count:
description:
- Number of aliases
returned: on success
type: int
ipv4_list:
description:
- List of IPv4 addresses
returned: on success
type: list
elements: str
ipv6_list:
description:
- List of IPv6 addresses
returned: on success
type: list
elements: str
api_url:
description: URL for the failed request
returned: on failure
type: str
status_code:
description: Status code for the request
returned: always
type: int
"""

EXAMPLES = """
- name: Assign two aliases to a segment
serverscom.sc_api.sc_l2_segment_aliases:
token: '{{ lookup("env", "SC_TOKEN") }}'
name: L2 example
count: 2
register: l2
- name: Report aliases
debug: var=l2.l2_segment.networks
- name: Remove aliases from a segment by ID
serverscom.sc_api.sc_l2_segment:
token: '{{ lookup("env", "SC_TOKEN") }}'
segment_id: 0m592Zmn
aliases_absent:
- 7p6bE0p3
- name: Remove aliases from a segment by value
serverscom.sc_api.sc_l2_segment:
token: '{{ lookup("env", "SC_TOKEN") }}'
segment_id: L2 example
aliases_absent:
- 203.0.113.41
- 203.0.113.42
- name: Remove all aliases from a segment
serverscom.sc_api.sc_l2_segment:
token: '{{ lookup("env", "SC_TOKEN") }}'
segment_id: L2 example
count: 0
""" # noqa


from ansible.module_utils.basic import AnsibleModule
from ansible_collections.serverscom.sc_api.plugins.module_utils.modules import (
DEFAULT_API_ENDPOINT,
SCBaseError,
ScL2SegmentAliases,
)


__metaclass__ = type


def main():
module = AnsibleModule(
argument_spec={
"token": {"type": "str", "no_log": True, "required": True},
"endpoint": {"default": DEFAULT_API_ENDPOINT},
"name": {},
"segment_id": {},
"count": {"type": "int"},
"aliases_absent": {
"type": "list",
"elements": "str",
},
"wait": {"type": "int", "default": 1200},
"update_interval": {"type": "int", "default": 30},
},
mutually_exclusive=[
("name", "segment_id"),
("count", "aliases_absent"),
],
required_one_of=[
("name", "segment_id"),
("count", "aliases_absent"),
],
supports_check_mode=True,
)
try:
sc_info = ScL2SegmentAliases(
endpoint=module.params["endpoint"],
token=module.params["token"],
name=module.params["name"],
segment_id=module.params["segment_id"],
count=module.params["count"],
aliases_absent=module.params["aliases_absent"],
wait=module.params["wait"],
update_interval=module.params["update_interval"],
checkmode=module.check_mode
)
module.exit_json(**sc_info.run())
except SCBaseError as e:
module.exit_json(**e.fail())


if __name__ == "__main__":
main()
Loading

0 comments on commit 538b089

Please sign in to comment.